This commit is contained in:
Zane Schepke 2023-12-24 18:09:23 -05:00
parent f0ec661223
commit 408d88390b
7 changed files with 114 additions and 122 deletions

View File

@ -22,6 +22,9 @@ interface SettingsDoa {
@Query("SELECT * FROM settings")
suspend fun getAll(): List<Settings>
@Query("SELECT * FROM settings LIMIT 1")
fun getSettingsFlow(): Flow<Settings>
@Query("SELECT * FROM settings")
fun getAllFlow(): Flow<MutableList<Settings>>

View File

@ -27,12 +27,12 @@ class DataStoreManager(private val context: Context) {
context.dataStore.edit {
it[key] = value
}
fun <T> getFromStore(key: Preferences.Key<T>) =
context.dataStore.data.map {
fun <T> getFromStoreFlow(key: Preferences.Key<T>) = context.dataStore.data.map {
it[key]
}
suspend fun <T> getFromStore(key: Preferences.Key<T>) = context.dataStore.data.first { it.contains(key) }[key]
val locationDisclosureFlow: Flow<Boolean?> = context.dataStore.data.map {
it[LOCATION_DISCLOSURE_SHOWN]
}

View File

@ -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()
// )
}

View File

@ -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<ActivityViewModel>()
// val activityViewModel = hiltViewModel<ActivityViewModel>()
val navController = rememberNavController()
val focusRequester = remember { FocusRequester() }

View File

@ -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,

View File

@ -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(

View File

@ -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<String>())
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()
ServiceManager.startWatcherService(application, defaultTunnel)
}
val defaultTunnel = _settings.value.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
)
)
}
}