From 408d88390b5ea4a105563a0467e943a1981685a3 Mon Sep 17 00:00:00 2001 From: Zane Schepke Date: Sun, 24 Dec 2023 18:09:23 -0500 Subject: [PATCH] refactor --- .../repository/SettingsDoa.kt | 3 + .../repository/datastore/DataStoreManager.kt | 6 +- .../ui/ActivityViewModel.kt | 15 +- .../wireguardautotunnel/ui/MainActivity.kt | 4 +- .../ui/screens/main/MainScreen.kt | 7 +- .../ui/screens/settings/SettingsScreen.kt | 55 +++---- .../ui/screens/settings/SettingsViewModel.kt | 146 +++++++----------- 7 files changed, 114 insertions(+), 122 deletions(-) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/repository/SettingsDoa.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/repository/SettingsDoa.kt index fbf116a..46c89ae 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/repository/SettingsDoa.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/repository/SettingsDoa.kt @@ -22,6 +22,9 @@ interface SettingsDoa { @Query("SELECT * FROM settings") suspend fun getAll(): List + @Query("SELECT * FROM settings LIMIT 1") + fun getSettingsFlow(): Flow + @Query("SELECT * FROM settings") fun getAllFlow(): Flow> diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/repository/datastore/DataStoreManager.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/repository/datastore/DataStoreManager.kt index f282c8f..e99c2f2 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/repository/datastore/DataStoreManager.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/repository/datastore/DataStoreManager.kt @@ -27,12 +27,12 @@ class DataStoreManager(private val context: Context) { context.dataStore.edit { it[key] = value } - - fun getFromStore(key: Preferences.Key) = - context.dataStore.data.map { + fun getFromStoreFlow(key: Preferences.Key) = context.dataStore.data.map { it[key] } + suspend fun getFromStore(key: Preferences.Key) = context.dataStore.data.first { it.contains(key) }[key] + val locationDisclosureFlow: Flow = context.dataStore.data.map { it[LOCATION_DISCLOSURE_SHOWN] } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/ActivityViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/ActivityViewModel.kt index d583e16..25f8820 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/ActivityViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/ActivityViewModel.kt @@ -1,8 +1,19 @@ package com.zaneschepke.wireguardautotunnel.ui import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa +import com.zaneschepke.wireguardautotunnel.repository.model.Settings +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.stateIn import javax.inject.Inject -class ActivityViewModel @Inject constructor() : ViewModel() { - // TODO move shared logic to shared viewmodel +@HiltViewModel +class ActivityViewModel @Inject constructor( + private val settingsRepo: SettingsDoa, +) : ViewModel() { +// val settings = settingsRepo.getSettingsFlow().stateIn(viewModelScope, +// SharingStarted.WhileSubscribed(5000L), Settings() +// ) } 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 40c766a..507b558 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt @@ -32,6 +32,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.input.key.onKeyEvent import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.lifecycleScope import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable @@ -64,8 +65,7 @@ class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { - // TODO move shared logic to shared viewmodel - // val sharedViewModel = hiltViewModel() +// val activityViewModel = hiltViewModel() val navController = rememberNavController() val focusRequester = remember { FocusRequester() } 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 3649c5c..53458fe 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 @@ -8,6 +8,8 @@ import android.os.Build import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.clickable @@ -84,6 +86,7 @@ import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus +import com.zaneschepke.wireguardautotunnel.ui.ActivityViewModel import com.zaneschepke.wireguardautotunnel.ui.CaptureActivityPortrait import com.zaneschepke.wireguardautotunnel.ui.Routes import com.zaneschepke.wireguardautotunnel.ui.common.RowListItem @@ -212,7 +215,7 @@ fun MainScreen( } ) - if (showPrimaryChangeAlertDialog) { + AnimatedVisibility(showPrimaryChangeAlertDialog) { AlertDialog( onDismissRequest = { showPrimaryChangeAlertDialog = false @@ -288,7 +291,7 @@ fun MainScreen( } } ) { - if (tunnels.isEmpty()) { + AnimatedVisibility(tunnels.isEmpty(), exit = fadeOut(), enter = fadeIn()) { Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, 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 7250ffc..8413ce1 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 @@ -38,11 +38,13 @@ import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.SideEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi @@ -71,6 +73,7 @@ import com.wireguard.android.backend.WgQuickBackend import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.repository.datastore.DataStoreManager +import com.zaneschepke.wireguardautotunnel.ui.ActivityViewModel import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt @@ -95,31 +98,29 @@ fun SettingsScreen( val scope = rememberCoroutineScope { Dispatchers.IO } val context = LocalContext.current val focusManager = LocalFocusManager.current + val scrollState = rememberScrollState() val keyboardController = LocalSoftwareKeyboardController.current val interactionSource = remember { MutableInteractionSource() } val settings by viewModel.settings.collectAsStateWithLifecycle() - val trustedSSIDs by viewModel.trustedSSIDs.collectAsStateWithLifecycle() - val tunnels by viewModel.tunnels.collectAsStateWithLifecycle(mutableListOf()) + val tunnels by viewModel.tunnels.collectAsStateWithLifecycle() + val vpnState = viewModel.vpnState.collectAsStateWithLifecycle() + val fineLocationState = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION) var currentText by remember { mutableStateOf("") } - val scrollState = rememberScrollState() var isBackgroundLocationGranted by remember { mutableStateOf(true) } - var showAuthPrompt by remember { mutableStateOf(false) } var didExportFiles by remember { mutableStateOf(false) } - val isLocationDisclosureShown by viewModel.disclosureShown.collectAsStateWithLifecycle( - null - ) - val vpnState = viewModel.vpnState.collectAsStateWithLifecycle(initialValue = Tunnel.State.DOWN) + var showAuthPrompt by remember { mutableStateOf(false) } + var isLocationDisclosureShown by rememberSaveable { + mutableStateOf(false) + } val screenPadding = 5.dp val fillMaxWidth = .85f - fun setLocationDisclosureShown() = scope.launch { - viewModel.dataStoreManager.saveToDataStore( - DataStoreManager.LOCATION_DISCLOSURE_SHOWN, - true - ) + + LaunchedEffect(Unit) { + isLocationDisclosureShown = viewModel.isLocationDisclosureShown() } fun exportAllConfigs() { @@ -175,25 +176,25 @@ fun SettingsScreen( isBackgroundLocationGranted = if (!backgroundLocationState.status.isGranted) { false } else { - SideEffect { - setLocationDisclosureShown() + if(!isLocationDisclosureShown) { + viewModel.setLocationDisclosureShown() } true } } if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { - if (!fineLocationState.status.isGranted) { - isBackgroundLocationGranted = false + isBackgroundLocationGranted = if (!fineLocationState.status.isGranted) { + false } else { SideEffect { - setLocationDisclosureShown() + viewModel.setLocationDisclosureShown() } - isBackgroundLocationGranted = true + true } } - if (isLocationDisclosureShown != true) { + AnimatedVisibility(!isLocationDisclosureShown) { Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Top, @@ -238,22 +239,21 @@ fun SettingsScreen( horizontalArrangement = Arrangement.SpaceEvenly ) { TextButton(onClick = { - setLocationDisclosureShown() + viewModel.setLocationDisclosureShown() }) { Text(stringResource(id = R.string.no_thanks)) } TextButton(modifier = Modifier.focusRequester(focusRequester), onClick = { openSettings() - setLocationDisclosureShown() + viewModel.setLocationDisclosureShown() }) { Text(stringResource(id = R.string.turn_on)) } } } - return } - if (showAuthPrompt) { + AnimatedVisibility(showAuthPrompt) { AuthorizationPrompt( onSuccess = { showAuthPrompt = false @@ -348,7 +348,7 @@ fun SettingsScreen( .fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(5.dp) ) { - trustedSSIDs.forEach { ssid -> + settings.trustedNetworkSSIDs.forEach { ssid -> ClickableIconButton( onIconClick = { scope.launch { @@ -360,7 +360,7 @@ fun SettingsScreen( enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled) ) } - if (trustedSSIDs.isEmpty()) { + if (settings.trustedNetworkSSIDs.isEmpty()) { Text( stringResource(R.string.none), fontStyle = FontStyle.Italic, @@ -535,7 +535,8 @@ fun SettingsScreen( shape = RoundedCornerShape(12.dp), color = MaterialTheme.colorScheme.surface, modifier = Modifier - .fillMaxWidth(fillMaxWidth).padding(vertical = 10.dp) + .fillMaxWidth(fillMaxWidth) + .padding(vertical = 10.dp) .padding(bottom = 140.dp) ) { Column( 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 959b9f6..3cb6025 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 @@ -6,22 +6,19 @@ import android.location.LocationManager import android.os.Build import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.wireguard.android.backend.Tunnel import com.wireguard.android.util.RootShell import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa import com.zaneschepke.wireguardautotunnel.repository.TunnelConfigDao import com.zaneschepke.wireguardautotunnel.repository.datastore.DataStoreManager import com.zaneschepke.wireguardautotunnel.repository.model.Settings -import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService import com.zaneschepke.wireguardautotunnel.util.WgTunnelException import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import timber.log.Timber @@ -32,110 +29,87 @@ constructor( private val application: Application, private val tunnelRepo: TunnelConfigDao, private val settingsRepo: SettingsDoa, - val dataStoreManager: DataStoreManager, + private val dataStoreManager: DataStoreManager, private val rootShell: RootShell, private val vpnService: VpnService ) : ViewModel() { - private val _trustedSSIDs = MutableStateFlow(emptyList()) - val trustedSSIDs = _trustedSSIDs.asStateFlow() - private val _settings = MutableStateFlow(Settings()) - val settings get() = _settings.asStateFlow() - val vpnState get() = vpnService.state - val tunnels get() = tunnelRepo.getAllFlow() - val disclosureShown = dataStoreManager.locationDisclosureFlow + val settings = settingsRepo.getSettingsFlow().stateIn(viewModelScope, + SharingStarted.WhileSubscribed(5_000L), Settings()) + val tunnels = tunnelRepo.getAllFlow().stateIn(viewModelScope, + SharingStarted.WhileSubscribed(5_000L), emptyList()) + val vpnState get() = vpnService.state.stateIn(viewModelScope, + SharingStarted.WhileSubscribed(5_000L), Tunnel.State.DOWN) - init { - isLocationServicesEnabled() - viewModelScope.launch(Dispatchers.IO) { - settingsRepo.getAllFlow().filter { it.isNotEmpty() }.collect { - val settings = it.first() - _settings.emit(settings) - _trustedSSIDs.emit(settings.trustedNetworkSSIDs.toList()) - } - } - } suspend fun onSaveTrustedSSID(ssid: String) { val trimmed = ssid.trim() - if (!_settings.value.trustedNetworkSSIDs.contains(trimmed)) { - _settings.value.trustedNetworkSSIDs.add(trimmed) - settingsRepo.save(_settings.value) + if (!settings.value.trustedNetworkSSIDs.contains(trimmed)) { + settings.value.trustedNetworkSSIDs.add(trimmed) + settingsRepo.save(settings.value) } else { throw WgTunnelException("SSID already exists.") } } + suspend fun isLocationDisclosureShown() : Boolean { + return dataStoreManager.getFromStore(DataStoreManager.LOCATION_DISCLOSURE_SHOWN) ?: false + } + + fun setLocationDisclosureShown() { + viewModelScope.launch { + dataStoreManager.saveToDataStore(DataStoreManager.LOCATION_DISCLOSURE_SHOWN, true) + } + } + suspend fun onToggleTunnelOnMobileData() { settingsRepo.save( - _settings.value.copy( - isTunnelOnMobileDataEnabled = !_settings.value.isTunnelOnMobileDataEnabled + settings.value.copy( + isTunnelOnMobileDataEnabled = !settings.value.isTunnelOnMobileDataEnabled ) ) } suspend fun onDeleteTrustedSSID(ssid: String) { - _settings.value.trustedNetworkSSIDs.remove(ssid) - settingsRepo.save(_settings.value) + settings.value.trustedNetworkSSIDs.remove(ssid) + settingsRepo.save(settings.value) } - private fun emitFirstTunnelAsDefault() = - viewModelScope.async { - _settings.emit(_settings.value.copy(defaultTunnel = getFirstTunnelConfig().toString())) - } + private suspend fun getDefaultTunnelOrFirst() : String { + return settings.value.defaultTunnel ?: tunnelRepo.getAll().first().wgQuick + } suspend fun toggleAutoTunnel() { - if (_settings.value.isAutoTunnelEnabled) { + val defaultTunnel = getDefaultTunnelOrFirst() + if (settings.value.isAutoTunnelEnabled) { ServiceManager.stopWatcherService(application) } else { - if (_settings.value.defaultTunnel == null) { - emitFirstTunnelAsDefault().await() - } - val defaultTunnel = _settings.value.defaultTunnel - ServiceManager.startWatcherService(application, defaultTunnel!!) + ServiceManager.startWatcherService(application, defaultTunnel) } - settingsRepo.save( - _settings.value.copy( - isAutoTunnelEnabled = !_settings.value.isAutoTunnelEnabled + saveSettings( + settings.value.copy( + isAutoTunnelEnabled = settings.value.isAutoTunnelEnabled, + defaultTunnel = defaultTunnel ) ) } - private suspend fun getFirstTunnelConfig(): TunnelConfig { - return tunnelRepo.getAll().first() - } - suspend fun onToggleAlwaysOnVPN() { - if (_settings.value.defaultTunnel == null) { - emitFirstTunnelAsDefault().await() - } val updatedSettings = - _settings.value.copy( - isAlwaysOnVpnEnabled = !_settings.value.isAlwaysOnVpnEnabled + settings.value.copy( + isAlwaysOnVpnEnabled = !settings.value.isAlwaysOnVpnEnabled, + defaultTunnel = getDefaultTunnelOrFirst() ) - emitSettings(updatedSettings) saveSettings(updatedSettings) } - private suspend fun emitSettings(settings: Settings) { - _settings.emit( - settings - ) - } - private suspend fun saveSettings(settings: Settings) { settingsRepo.save(settings) } suspend fun onToggleTunnelOnEthernet() { - if (_settings.value.defaultTunnel == null) { - emitFirstTunnelAsDefault().await() - } - _settings.emit( - _settings.value.copy( - isTunnelOnEthernetEnabled = !_settings.value.isTunnelOnEthernetEnabled - ) - ) - settingsRepo.save(_settings.value) + saveSettings(settings.value.copy( + isTunnelOnEthernetEnabled = !settings.value.isTunnelOnEthernetEnabled + )) } private fun isLocationServicesEnabled(): Boolean { @@ -149,31 +123,39 @@ constructor( } suspend fun onToggleShortcutsEnabled() { - settingsRepo.save( - _settings.value.copy( - isShortcutsEnabled = !_settings.value.isShortcutsEnabled + saveSettings( + settings.value.copy( + isShortcutsEnabled = !settings.value.isShortcutsEnabled ) ) } suspend fun onToggleBatterySaver() { - settingsRepo.save( - _settings.value.copy( - isBatterySaverEnabled = !_settings.value.isBatterySaverEnabled + saveSettings( + settings.value.copy( + isBatterySaverEnabled = !settings.value.isBatterySaverEnabled ) ) } private suspend fun saveKernelMode(on: Boolean) { - settingsRepo.save( - _settings.value.copy( + saveSettings( + settings.value.copy( isKernelEnabled = on ) ) } + suspend fun onToggleTunnelOnWifi() { + saveSettings( + settings.value.copy( + isTunnelOnWifiEnabled = !settings.value.isTunnelOnWifiEnabled + ) + ) + } + suspend fun onToggleKernelMode() { - if (!_settings.value.isKernelEnabled) { + if (!settings.value.isKernelEnabled) { try { rootShell.start() Timber.d("Root shell accepted!") @@ -186,12 +168,4 @@ constructor( saveKernelMode(on = false) } } - - suspend fun onToggleTunnelOnWifi() { - settingsRepo.save( - _settings.value.copy( - isTunnelOnWifiEnabled = !_settings.value.isTunnelOnWifiEnabled - ) - ) - } }