feat: user logger toggle control

Allow users to toggle on and off local logger. This will hopefully help address #437
Allow shortcuts activation on AndroidTV
closes #451
This commit is contained in:
Zane Schepke 2024-11-22 21:06:58 -05:00
parent b2938c3ac8
commit f9ef308f8a
13 changed files with 297 additions and 202 deletions

View File

@ -6,6 +6,7 @@ import android.os.StrictMode
import android.os.StrictMode.ThreadPolicy import android.os.StrictMode.ThreadPolicy
import com.zaneschepke.logcatter.LogReader import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.wireguardautotunnel.data.datastore.LocaleStorage import com.zaneschepke.wireguardautotunnel.data.datastore.LocaleStorage
import com.zaneschepke.wireguardautotunnel.data.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.util.LocaleUtil import com.zaneschepke.wireguardautotunnel.util.LocaleUtil
@ -32,6 +33,9 @@ class WireGuardAutoTunnel : Application() {
@Inject @Inject
lateinit var logReader: LogReader lateinit var logReader: LogReader
@Inject
lateinit var appStateRepository: AppStateRepository
@Inject @Inject
@IoDispatcher @IoDispatcher
lateinit var ioDispatcher: CoroutineDispatcher lateinit var ioDispatcher: CoroutineDispatcher
@ -54,10 +58,13 @@ class WireGuardAutoTunnel : Application() {
} }
if (!isRunningOnTv()) { if (!isRunningOnTv()) {
applicationScope.launch(ioDispatcher) { applicationScope.launch(ioDispatcher) {
if (appStateRepository.isLocalLogsEnabled()) {
Timber.d("Starting logger")
logReader.start() logReader.start()
} }
} }
} }
}
override fun attachBaseContext(base: Context) { override fun attachBaseContext(base: Context) {
super.attachBaseContext(LocaleUtil.getLocalizedContext(base, LocaleStorage(base).getPreferredLocale())) super.attachBaseContext(LocaleUtil.getLocalizedContext(base, LocaleStorage(base).getPreferredLocale()))

View File

@ -26,6 +26,7 @@ class DataStoreManager(
val currentSSID = stringPreferencesKey("CURRENT_SSID") val currentSSID = stringPreferencesKey("CURRENT_SSID")
val pinLockEnabled = booleanPreferencesKey("PIN_LOCK_ENABLED") val pinLockEnabled = booleanPreferencesKey("PIN_LOCK_ENABLED")
val tunnelStatsExpanded = booleanPreferencesKey("TUNNEL_STATS_EXPANDED") val tunnelStatsExpanded = booleanPreferencesKey("TUNNEL_STATS_EXPANDED")
val isLocalLogsEnabled = booleanPreferencesKey("LOCAL_LOGS_ENABLED")
val theme = stringPreferencesKey("THEME") val theme = stringPreferencesKey("THEME")
} }

View File

@ -7,6 +7,7 @@ data class GeneralState(
val isBatteryOptimizationDisableShown: Boolean = BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT, val isBatteryOptimizationDisableShown: Boolean = BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT,
val isPinLockEnabled: Boolean = PIN_LOCK_ENABLED_DEFAULT, val isPinLockEnabled: Boolean = PIN_LOCK_ENABLED_DEFAULT,
val isTunnelStatsExpanded: Boolean = IS_TUNNEL_STATS_EXPANDED, val isTunnelStatsExpanded: Boolean = IS_TUNNEL_STATS_EXPANDED,
val isLocalLogsEnabled: Boolean = IS_LOGS_ENABLED_DEFAULT,
val theme: Theme = Theme.AUTOMATIC, val theme: Theme = Theme.AUTOMATIC,
) { ) {
companion object { companion object {
@ -14,5 +15,6 @@ data class GeneralState(
const val BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT = false const val BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT = false
const val PIN_LOCK_ENABLED_DEFAULT = false const val PIN_LOCK_ENABLED_DEFAULT = false
const val IS_TUNNEL_STATS_EXPANDED = false const val IS_TUNNEL_STATS_EXPANDED = false
const val IS_LOGS_ENABLED_DEFAULT = false
} }
} }

View File

@ -29,5 +29,9 @@ interface AppStateRepository {
suspend fun getTheme(): Theme suspend fun getTheme(): Theme
suspend fun isLocalLogsEnabled(): Boolean
suspend fun setLocalLogsEnabled(enabled: Boolean)
val generalStateFlow: Flow<GeneralState> val generalStateFlow: Flow<GeneralState>
} }

View File

@ -69,6 +69,14 @@ class DataStoreAppStateRepository(
} ?: Theme.AUTOMATIC } ?: Theme.AUTOMATIC
} }
override suspend fun isLocalLogsEnabled(): Boolean {
return dataStoreManager.getFromStore(DataStoreManager.isLocalLogsEnabled) ?: GeneralState.IS_LOGS_ENABLED_DEFAULT
}
override suspend fun setLocalLogsEnabled(enabled: Boolean) {
dataStoreManager.saveToDataStore(DataStoreManager.isLocalLogsEnabled, enabled)
}
override val generalStateFlow: Flow<GeneralState> = override val generalStateFlow: Flow<GeneralState> =
dataStoreManager.preferencesFlow.map { prefs -> dataStoreManager.preferencesFlow.map { prefs ->
prefs?.let { pref -> prefs?.let { pref ->
@ -84,6 +92,7 @@ class DataStoreAppStateRepository(
pref[DataStoreManager.pinLockEnabled] pref[DataStoreManager.pinLockEnabled]
?: GeneralState.PIN_LOCK_ENABLED_DEFAULT, ?: GeneralState.PIN_LOCK_ENABLED_DEFAULT,
isTunnelStatsExpanded = pref[DataStoreManager.tunnelStatsExpanded] ?: GeneralState.IS_TUNNEL_STATS_EXPANDED, isTunnelStatsExpanded = pref[DataStoreManager.tunnelStatsExpanded] ?: GeneralState.IS_TUNNEL_STATS_EXPANDED,
isLocalLogsEnabled = pref[DataStoreManager.isLocalLogsEnabled] ?: GeneralState.IS_LOGS_ENABLED_DEFAULT,
theme = getTheme(), theme = getTheme(),
) )
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {

View File

@ -49,7 +49,7 @@ constructor(
vpnState.copy( vpnState.copy(
tunnelConfig = tunnels.firstOrNull { it.id == vpnState.tunnelConfig?.id }, tunnelConfig = tunnels.firstOrNull { it.id == vpnState.tunnelConfig?.id },
) )
}.stateIn(applicationScope, SharingStarted.Lazily, VpnState()) }.stateIn(applicationScope, SharingStarted.Eagerly, VpnState())
private var statsJob: Job? = null private var statsJob: Job? = null

View File

@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.wireguard.android.backend.WgQuickBackend import com.wireguard.android.backend.WgQuickBackend
import com.wireguard.android.util.RootShell import com.wireguard.android.util.RootShell
import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
@ -25,6 +26,7 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.takeWhile import kotlinx.coroutines.flow.takeWhile
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.plus import kotlinx.coroutines.plus
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -41,6 +43,7 @@ constructor(
@IoDispatcher private val ioDispatcher: CoroutineDispatcher, @IoDispatcher private val ioDispatcher: CoroutineDispatcher,
@AppShell private val rootShell: Provider<RootShell>, @AppShell private val rootShell: Provider<RootShell>,
private val serviceManager: ServiceManager, private val serviceManager: ServiceManager,
private val logReader: LogReader,
) : ViewModel() { ) : ViewModel() {
val uiState = val uiState =
@ -64,11 +67,14 @@ constructor(
AppUiState(), AppUiState(),
) )
private val _isAppReady = MutableStateFlow<Boolean>(false) private val _isAppReady = MutableStateFlow(false)
val isAppReady = _isAppReady.asStateFlow() val isAppReady = _isAppReady.asStateFlow()
private val _configurationChange = MutableStateFlow(false)
val configurationChange = _configurationChange.asStateFlow()
init { init {
viewModelScope.launch { viewModelScope.launch(ioDispatcher) {
initPin() initPin()
initAutoTunnel() initAutoTunnel()
initTunnel() initTunnel()
@ -84,7 +90,6 @@ constructor(
} }
private suspend fun initTunnel() { private suspend fun initTunnel() {
if (tunnelService.get().getState() == TunnelState.UP) tunnelService.get().startStatsJob()
val activeTunnels = appDataRepository.tunnels.getActive() val activeTunnels = appDataRepository.tunnels.getActive()
if (activeTunnels.isNotEmpty() && if (activeTunnels.isNotEmpty() &&
tunnelService.get().getState() == TunnelState.DOWN tunnelService.get().getState() == TunnelState.DOWN
@ -116,6 +121,22 @@ constructor(
appDataRepository.appState.setLocationDisclosureShown(true) appDataRepository.appState.setLocationDisclosureShown(true)
} }
fun onToggleLocalLogging() = viewModelScope.launch(ioDispatcher) {
with(uiState.value.generalState) {
val toggledOn = !isLocalLogsEnabled
appDataRepository.appState.setLocalLogsEnabled(toggledOn)
if (!toggledOn) onLoggerStop()
_configurationChange.update {
true
}
}
}
private suspend fun onLoggerStop() {
logReader.stop()
logReader.deleteAndClearLogs()
}
fun onToggleAlwaysOnVPN() = viewModelScope.launch { fun onToggleAlwaysOnVPN() = viewModelScope.launch {
with(uiState.value.settings) { with(uiState.value.settings) {
appDataRepository.settings.save( appDataRepository.settings.save(

View File

@ -1,6 +1,7 @@
package com.zaneschepke.wireguardautotunnel.ui package com.zaneschepke.wireguardautotunnel.ui
import android.content.Context import android.content.Context
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.viewModels import androidx.activity.viewModels
@ -24,9 +25,7 @@ import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
@ -42,7 +41,6 @@ import com.zaneschepke.wireguardautotunnel.data.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavItem import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavItem
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalFocusRequester
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.CustomSnackBar import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.CustomSnackBar
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarControllerProvider import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarControllerProvider
@ -65,8 +63,8 @@ import com.zaneschepke.wireguardautotunnel.util.LocaleUtil
import com.zaneschepke.wireguardautotunnel.util.extensions.requestAutoTunnelTileServiceUpdate import com.zaneschepke.wireguardautotunnel.util.extensions.requestAutoTunnelTileServiceUpdate
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import kotlin.system.exitProcess
@AndroidEntryPoint @AndroidEntryPoint
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
@ -83,11 +81,11 @@ class MainActivity : AppCompatActivity() {
@Inject @Inject
lateinit var tunnelService: TunnelService lateinit var tunnelService: TunnelService
private val viewModel by viewModels<AppViewModel>()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val viewModel by viewModels<AppViewModel>()
installSplashScreen().apply { installSplashScreen().apply {
setKeepOnScreenCondition { setKeepOnScreenCondition {
!viewModel.isAppReady.value !viewModel.isAppReady.value
@ -95,15 +93,23 @@ class MainActivity : AppCompatActivity() {
} }
setContent { setContent {
val appUiState by viewModel.uiState.collectAsStateWithLifecycle(lifecycle = this.lifecycle) val appUiState by viewModel.uiState.collectAsStateWithLifecycle()
val configurationChange by viewModel.configurationChange.collectAsStateWithLifecycle()
val navController = rememberNavController() val navController = rememberNavController()
val rootItemFocusRequester = remember { FocusRequester() }
LaunchedEffect(appUiState.tunnels) { LaunchedEffect(appUiState.tunnels) {
Timber.d("Updating launched")
requestTunnelTileServiceStateUpdate() requestTunnelTileServiceStateUpdate()
} }
LaunchedEffect(configurationChange) {
if (configurationChange) {
Intent(this@MainActivity, MainActivity::class.java).also {
startActivity(it)
exitProcess(0)
}
}
}
LaunchedEffect(appUiState.autoTunnelActive) { LaunchedEffect(appUiState.autoTunnelActive) {
requestAutoTunnelTileServiceUpdate() requestAutoTunnelTileServiceUpdate()
} }
@ -114,7 +120,6 @@ class MainActivity : AppCompatActivity() {
} }
} }
CompositionLocalProvider(LocalFocusRequester provides rootItemFocusRequester) {
CompositionLocalProvider(LocalNavController provides navController) { CompositionLocalProvider(LocalNavController provides navController) {
SnackbarControllerProvider { host -> SnackbarControllerProvider { host ->
WireguardAutoTunnelTheme(theme = appUiState.generalState.theme) { WireguardAutoTunnelTheme(theme = appUiState.generalState.theme) {
@ -191,7 +196,7 @@ class MainActivity : AppCompatActivity() {
DisplayScreen(appUiState) DisplayScreen(appUiState)
} }
composable<Route.Support> { composable<Route.Support> {
SupportScreen() SupportScreen(appUiState, viewModel)
} }
composable<Route.Logs> { composable<Route.Logs> {
LogsScreen() LogsScreen()
@ -225,7 +230,6 @@ class MainActivity : AppCompatActivity() {
} }
} }
} }
}
override fun attachBaseContext(newBase: Context) { override fun attachBaseContext(newBase: Context) {
oldPrefLocaleCode = LocaleStorage(newBase).getPreferredLocale() oldPrefLocaleCode = LocaleStorage(newBase).getPreferredLocale()
@ -241,9 +245,4 @@ class MainActivity : AppCompatActivity() {
} }
super.onResume() super.onResume()
} }
override fun onDestroy() {
super.onDestroy()
tunnelService.cancelStatsJob()
}
} }

View File

@ -1,11 +1,8 @@
package com.zaneschepke.wireguardautotunnel.ui.common.navigation package com.zaneschepke.wireguardautotunnel.ui.common.navigation
import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.focus.FocusRequester
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
val LocalNavController = compositionLocalOf<NavHostController> { val LocalNavController = compositionLocalOf<NavHostController> {
error("NavController was not provided") error("NavController was not provided")
} }
val LocalFocusRequester = compositionLocalOf<FocusRequester> { error("FocusRequester is not provided") }

View File

@ -2,7 +2,6 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.options
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
@ -53,7 +52,6 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.isValidIpv4orIpv6Addr
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
@OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
fun OptionsScreen(optionsViewModel: OptionsViewModel = hiltViewModel(), appUiState: AppUiState, tunnelId: Int) { fun OptionsScreen(optionsViewModel: OptionsViewModel = hiltViewModel(), appUiState: AppUiState, tunnelId: Int) {
val navController = LocalNavController.current val navController = LocalNavController.current

View File

@ -5,7 +5,6 @@ import androidx.compose.foundation.focusable
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
@ -30,13 +29,11 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
import com.zaneschepke.wireguardautotunnel.ui.AppUiState import com.zaneschepke.wireguardautotunnel.ui.AppUiState
@ -45,7 +42,6 @@ import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalFocusRequester
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
@ -59,17 +55,12 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
import com.zaneschepke.wireguardautotunnel.util.extensions.showToast import com.zaneschepke.wireguardautotunnel.util.extensions.showToast
import xyz.teamgravity.pin_lock_compose.PinManager import xyz.teamgravity.pin_lock_compose.PinManager
@OptIn(
ExperimentalPermissionsApi::class,
ExperimentalLayoutApi::class,
)
@Composable @Composable
fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel: AppViewModel, uiState: AppUiState) { fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel: AppViewModel, uiState: AppUiState) {
val context = LocalContext.current val context = LocalContext.current
val navController = LocalNavController.current val navController = LocalNavController.current
val focusManager = LocalFocusManager.current val focusManager = LocalFocusManager.current
val snackbar = SnackbarController.current val snackbar = SnackbarController.current
val rootFocusRequester = LocalFocusRequester.current
val isRunningOnTv = remember { context.isRunningOnTv() } val isRunningOnTv = remember { context.isRunningOnTv() }
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
@ -135,16 +126,14 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel:
navController.navigate(Route.AutoTunnel) navController.navigate(Route.AutoTunnel)
}, },
trailing = { trailing = {
ForwardButton(Modifier.focusable().focusRequester(rootFocusRequester)) { navController.navigate(Route.AutoTunnel) } ForwardButton(Modifier.focusable()) { navController.navigate(Route.AutoTunnel) }
}, },
), ),
), ),
) )
SurfaceSelectionGroupButton( SurfaceSelectionGroupButton(
buildList { buildList {
if (!isRunningOnTv) { add(
addAll(
listOf(
SelectionItem( SelectionItem(
Icons.Filled.AppShortcut, Icons.Filled.AppShortcut,
{ {
@ -161,6 +150,10 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel:
}, },
onClick = { appViewModel.onToggleShortcutsEnabled() }, onClick = { appViewModel.onToggleShortcutsEnabled() },
), ),
)
if (!isRunningOnTv) {
addAll(
listOf(
SelectionItem( SelectionItem(
Icons.Outlined.VpnLock, Icons.Outlined.VpnLock,
{ {

View File

@ -9,12 +9,17 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AttachMoney import androidx.compose.material.icons.filled.AttachMoney
import androidx.compose.material.icons.filled.Book import androidx.compose.material.icons.filled.Book
import androidx.compose.material.icons.filled.LineStyle
import androidx.compose.material.icons.filled.Mail import androidx.compose.material.icons.filled.Mail
import androidx.compose.material.icons.filled.Policy import androidx.compose.material.icons.filled.Policy
import androidx.compose.material.icons.filled.ViewTimeline
import androidx.compose.material.icons.outlined.ViewHeadline
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
@ -24,24 +29,42 @@ import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.BuildConfig import com.zaneschepke.wireguardautotunnel.BuildConfig
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.AppUiState
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
import com.zaneschepke.wireguardautotunnel.ui.Route import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
import com.zaneschepke.wireguardautotunnel.ui.common.label.VersionLabel import com.zaneschepke.wireguardautotunnel.ui.common.label.VersionLabel
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.ForwardButton import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.ForwardButton
import com.zaneschepke.wireguardautotunnel.ui.theme.topPadding import com.zaneschepke.wireguardautotunnel.ui.theme.topPadding
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
import com.zaneschepke.wireguardautotunnel.util.extensions.launchSupportEmail import com.zaneschepke.wireguardautotunnel.util.extensions.launchSupportEmail
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
@Composable @Composable
fun SupportScreen() { fun SupportScreen(appUiState: AppUiState, appViewModel: AppViewModel) {
val context = LocalContext.current val context = LocalContext.current
val navController = LocalNavController.current val navController = LocalNavController.current
var showDialog by remember { mutableStateOf(false) }
if (showDialog) {
InfoDialog(onAttest = {
showDialog = false
appViewModel.onToggleLocalLogging()
}, onDismiss = {
showDialog = false
}, title = {
Text(stringResource(R.string.configuration_change))
}, body = { Text(stringResource(R.string.requires_app_relaunch)) }, confirmText = { Text(stringResource(R.string.yes)) })
}
Column( Column(
horizontalAlignment = Alignment.Start, horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top), verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top),
@ -54,7 +77,8 @@ fun SupportScreen() {
) { ) {
GroupLabel(stringResource(R.string.thank_you)) GroupLabel(stringResource(R.string.thank_you))
SurfaceSelectionGroupButton( SurfaceSelectionGroupButton(
listOf( buildList {
add(
SelectionItem( SelectionItem(
Icons.Filled.Book, Icons.Filled.Book,
title = { title = {
@ -70,8 +94,40 @@ fun SupportScreen() {
context.openWebUrl(context.getString(R.string.docs_url)) context.openWebUrl(context.getString(R.string.docs_url))
}, },
), ),
)
if (!context.isRunningOnTv()) {
add(
SelectionItem( SelectionItem(
Icons.Filled.LineStyle, Icons.Outlined.ViewHeadline,
title = {
Text(
stringResource(R.string.local_logging),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
description = {
Text(
stringResource(R.string.enable_local_logging),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.outline),
)
},
trailing = {
ScaledSwitch(
appUiState.generalState.isLocalLogsEnabled,
onClick = {
showDialog = true
},
)
},
onClick = {
showDialog = true
},
),
)
if (appUiState.generalState.isLocalLogsEnabled) {
add(
SelectionItem(
Icons.Filled.ViewTimeline,
title = { title = {
Text( Text(
stringResource(R.string.read_logs), stringResource(R.string.read_logs),
@ -87,6 +143,10 @@ fun SupportScreen() {
navController.navigate(Route.Logs) navController.navigate(Route.Logs)
}, },
), ),
)
}
}
add(
SelectionItem( SelectionItem(
Icons.Filled.Policy, Icons.Filled.Policy,
title = { title = {
@ -102,8 +162,8 @@ fun SupportScreen() {
context.openWebUrl(context.getString(R.string.privacy_policy_url)) context.openWebUrl(context.getString(R.string.privacy_policy_url))
}, },
), ),
)
), },
) )
SurfaceSelectionGroupButton( SurfaceSelectionGroupButton(
buildList { buildList {

View File

@ -229,4 +229,8 @@
<string name="tunnel_running">Tunnel running</string> <string name="tunnel_running">Tunnel running</string>
<string name="monitoring_state_changes">Monitoring state changes</string> <string name="monitoring_state_changes">Monitoring state changes</string>
<string name="donate">Donate to project</string> <string name="donate">Donate to project</string>
<string name="local_logging">Local logging</string>
<string name="enable_local_logging">Enable local logging</string>
<string name="configuration_change">Configuration change</string>
<string name="requires_app_relaunch">This change requires an app relaunch. Would you like to proceed?</string>
</resources> </resources>