diff --git a/app/build.gradle.kts b/app/build.gradle.kts index cd898e2..202299a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -44,6 +44,8 @@ android { getByName("debug").assets.srcDirs(files("$projectDir/schemas")) // Room } + buildConfigField("String[]", "LANGUAGES", "new String[]{ ${languageList().joinToString(separator = ", ") { "\"$it\"" }} }") + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { useSupportLibrary = true } } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 780c147..1ae1216 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,13 +3,6 @@ xmlns:tools="http://schemas.android.com/tools"> - - @@ -19,7 +12,8 @@ - + diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/WireGuardAutoTunnel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/WireGuardAutoTunnel.kt index f00e04a..7a6935b 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/WireGuardAutoTunnel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/WireGuardAutoTunnel.kt @@ -1,11 +1,14 @@ package com.zaneschepke.wireguardautotunnel import android.app.Application +import android.content.Context import android.os.StrictMode import android.os.StrictMode.ThreadPolicy import com.zaneschepke.logcatter.LogReader +import com.zaneschepke.wireguardautotunnel.data.datastore.LocaleStorage import com.zaneschepke.wireguardautotunnel.module.ApplicationScope import com.zaneschepke.wireguardautotunnel.module.IoDispatcher +import com.zaneschepke.wireguardautotunnel.util.LocaleUtil import com.zaneschepke.wireguardautotunnel.util.ReleaseTree import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv import dagger.hilt.android.HiltAndroidApp @@ -18,6 +21,10 @@ import javax.inject.Inject @HiltAndroidApp class WireGuardAutoTunnel : Application() { + val localeStorage: LocaleStorage by lazy { + LocaleStorage(this) + } + @Inject @ApplicationScope lateinit var applicationScope: CoroutineScope @@ -52,6 +59,10 @@ class WireGuardAutoTunnel : Application() { } } + override fun attachBaseContext(base: Context) { + super.attachBaseContext(LocaleUtil.getLocalizedContext(base, LocaleStorage(base).getPreferredLocale())) + } + companion object { lateinit var instance: WireGuardAutoTunnel private set 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 c4ac9c3..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 @@ -21,11 +21,12 @@ class DataStoreManager( @IoDispatcher private val ioDispatcher: CoroutineDispatcher, ) { companion object { - val LOCATION_DISCLOSURE_SHOWN = booleanPreferencesKey("LOCATION_DISCLOSURE_SHOWN") - val BATTERY_OPTIMIZE_DISABLE_SHOWN = booleanPreferencesKey("BATTERY_OPTIMIZE_DISABLE_SHOWN") - val CURRENT_SSID = stringPreferencesKey("CURRENT_SSID") - val IS_PIN_LOCK_ENABLED = booleanPreferencesKey("PIN_LOCK_ENABLED") - val IS_TUNNEL_STATS_EXPANDED = booleanPreferencesKey("TUNNEL_STATS_EXPANDED") + val locationDisclosureShown = booleanPreferencesKey("LOCATION_DISCLOSURE_SHOWN") + val batteryDisableShown = booleanPreferencesKey("BATTERY_OPTIMIZE_DISABLE_SHOWN") + val currentSSID = stringPreferencesKey("CURRENT_SSID") + val pinLockEnabled = booleanPreferencesKey("PIN_LOCK_ENABLED") + val tunnelStatsExpanded = booleanPreferencesKey("TUNNEL_STATS_EXPANDED") + val theme = stringPreferencesKey("THEME") } // preferences diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/datastore/LocalStorage.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/datastore/LocalStorage.kt new file mode 100644 index 0000000..1e6b446 --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/datastore/LocalStorage.kt @@ -0,0 +1,17 @@ +package com.zaneschepke.wireguardautotunnel.data.datastore + +import android.content.Context +import android.content.SharedPreferences +import com.zaneschepke.wireguardautotunnel.util.LocaleUtil + +class LocaleStorage(context: Context) { + private var preferences: SharedPreferences = context.getSharedPreferences("sp", Context.MODE_PRIVATE) + + fun getPreferredLocale(): String { + return preferences.getString("preferred_locale", LocaleUtil.OPTION_PHONE_LANGUAGE)!! + } + + fun setPreferredLocale(localeCode: String) { + preferences.edit().putString("preferred_locale", localeCode).apply() + } +} 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 cc9d839..f53ca1f 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 @@ -1,10 +1,13 @@ package com.zaneschepke.wireguardautotunnel.data.domain +import com.zaneschepke.wireguardautotunnel.ui.theme.Theme + data class GeneralState( val isLocationDisclosureShown: Boolean = LOCATION_DISCLOSURE_SHOWN_DEFAULT, val isBatteryOptimizationDisableShown: Boolean = BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT, val isPinLockEnabled: Boolean = PIN_LOCK_ENABLED_DEFAULT, val isTunnelStatsExpanded: Boolean = IS_TUNNEL_STATS_EXPANDED, + val theme: Theme = Theme.AUTOMATIC ) { companion object { const val LOCATION_DISCLOSURE_SHOWN_DEFAULT = 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 9532d46..82e2cfd 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 @@ -1,6 +1,7 @@ package com.zaneschepke.wireguardautotunnel.data.repository import com.zaneschepke.wireguardautotunnel.data.domain.GeneralState +import com.zaneschepke.wireguardautotunnel.ui.theme.Theme import kotlinx.coroutines.flow.Flow interface AppStateRepository { @@ -24,5 +25,9 @@ interface AppStateRepository { suspend fun setTunnelStatsExpanded(expanded: Boolean) + suspend fun setTheme(theme: 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 918a3ca..06ac943 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/DataStoreAppStateRepository.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/DataStoreAppStateRepository.kt @@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.data.repository import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager import com.zaneschepke.wireguardautotunnel.data.domain.GeneralState +import com.zaneschepke.wireguardautotunnel.ui.theme.Theme import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import timber.log.Timber @@ -11,47 +12,61 @@ class DataStoreAppStateRepository( ) : AppStateRepository { override suspend fun isLocationDisclosureShown(): Boolean { - return dataStoreManager.getFromStore(DataStoreManager.LOCATION_DISCLOSURE_SHOWN) + return dataStoreManager.getFromStore(DataStoreManager.locationDisclosureShown) ?: GeneralState.LOCATION_DISCLOSURE_SHOWN_DEFAULT } override suspend fun setLocationDisclosureShown(shown: Boolean) { - dataStoreManager.saveToDataStore(DataStoreManager.LOCATION_DISCLOSURE_SHOWN, shown) + dataStoreManager.saveToDataStore(DataStoreManager.locationDisclosureShown, shown) } override suspend fun isPinLockEnabled(): Boolean { - return dataStoreManager.getFromStore(DataStoreManager.IS_PIN_LOCK_ENABLED) + return dataStoreManager.getFromStore(DataStoreManager.pinLockEnabled) ?: GeneralState.PIN_LOCK_ENABLED_DEFAULT } override suspend fun setPinLockEnabled(enabled: Boolean) { - dataStoreManager.saveToDataStore(DataStoreManager.IS_PIN_LOCK_ENABLED, enabled) + dataStoreManager.saveToDataStore(DataStoreManager.pinLockEnabled, enabled) } override suspend fun isBatteryOptimizationDisableShown(): Boolean { - return dataStoreManager.getFromStore(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN) + return dataStoreManager.getFromStore(DataStoreManager.batteryDisableShown) ?: GeneralState.BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT } override suspend fun setBatteryOptimizationDisableShown(shown: Boolean) { - dataStoreManager.saveToDataStore(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN, shown) + dataStoreManager.saveToDataStore(DataStoreManager.batteryDisableShown, shown) } override suspend fun getCurrentSsid(): String? { - return dataStoreManager.getFromStore(DataStoreManager.CURRENT_SSID) + return dataStoreManager.getFromStore(DataStoreManager.currentSSID) } override suspend fun setCurrentSsid(ssid: String) { - dataStoreManager.saveToDataStore(DataStoreManager.CURRENT_SSID, ssid) + dataStoreManager.saveToDataStore(DataStoreManager.currentSSID, ssid) } override suspend fun isTunnelStatsExpanded(): Boolean { - return dataStoreManager.getFromStore(DataStoreManager.IS_TUNNEL_STATS_EXPANDED) + return dataStoreManager.getFromStore(DataStoreManager.tunnelStatsExpanded) ?: GeneralState.IS_TUNNEL_STATS_EXPANDED } override suspend fun setTunnelStatsExpanded(expanded: Boolean) { - dataStoreManager.saveToDataStore(DataStoreManager.IS_TUNNEL_STATS_EXPANDED, expanded) + dataStoreManager.saveToDataStore(DataStoreManager.tunnelStatsExpanded, expanded) + } + + override suspend fun setTheme(theme: Theme) { + dataStoreManager.saveToDataStore(DataStoreManager.theme, theme.name) + } + + override suspend fun getTheme(): Theme { + return dataStoreManager.getFromStore(DataStoreManager.theme)?.let { + try { + Theme.valueOf(it) + } catch (_ : IllegalArgumentException) { + Theme.AUTOMATIC + } + } ?: Theme.AUTOMATIC } override val generalStateFlow: Flow = @@ -60,15 +75,16 @@ class DataStoreAppStateRepository( try { GeneralState( isLocationDisclosureShown = - pref[DataStoreManager.LOCATION_DISCLOSURE_SHOWN] + pref[DataStoreManager.locationDisclosureShown] ?: GeneralState.LOCATION_DISCLOSURE_SHOWN_DEFAULT, isBatteryOptimizationDisableShown = - pref[DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN] + pref[DataStoreManager.batteryDisableShown] ?: GeneralState.BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT, isPinLockEnabled = - pref[DataStoreManager.IS_PIN_LOCK_ENABLED] + pref[DataStoreManager.pinLockEnabled] ?: GeneralState.PIN_LOCK_ENABLED_DEFAULT, - isTunnelStatsExpanded = pref[DataStoreManager.IS_TUNNEL_STATS_EXPANDED] ?: GeneralState.IS_TUNNEL_STATS_EXPANDED, + isTunnelStatsExpanded = pref[DataStoreManager.tunnelStatsExpanded] ?: GeneralState.IS_TUNNEL_STATS_EXPANDED, + theme = getTheme() ) } catch (e: IllegalArgumentException) { Timber.e(e) 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 04cdace..3c4a6be 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppUiState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppUiState.kt @@ -10,4 +10,6 @@ data class AppUiState( val tunnels: List = emptyList(), val vpnState: VpnState = VpnState(), val generalState: GeneralState = GeneralState(), + val isKernelAvailable: Boolean = false, + val isRooted: 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 39a5074..cad3e47 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppViewModel.kt @@ -2,6 +2,8 @@ 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 @@ -18,10 +20,13 @@ 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 @@ -32,19 +37,34 @@ class AppViewModel constructor( private val appDataRepository: AppDataRepository, private val tunnelService: Provider, + private val rootShell: Provider, @IoDispatcher private val ioDispatcher: CoroutineDispatcher, ) : ViewModel() { private val _appUiState = MutableStateFlow(AppUiState()) + val appUiState = _appUiState.onStart { + _appUiState.update { + it.copy( + isRooted = isRooted(), + isKernelAvailable = isKernelSupported(), + ) + } + }.stateIn( + viewModelScope + ioDispatcher, + SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT), + AppUiState(), + ) + val uiState = combine( appDataRepository.settings.getSettingsFlow(), appDataRepository.tunnels.getTunnelConfigsFlow(), tunnelService.get().vpnState, appDataRepository.appState.generalStateFlow, - ) { settings, tunnels, tunnelState, generalState -> - AppUiState( + appUiState, + ) { settings, tunnels, tunnelState, generalState, appUiState -> + appUiState.copy( settings, tunnels, tunnelState, @@ -112,4 +132,21 @@ constructor( fun onPinLockEnabled() = viewModelScope.launch { appDataRepository.appState.setPinLockEnabled(true) } + + private suspend fun isKernelSupported(): Boolean { + return withContext(ioDispatcher) { + WgQuickBackend.hasKernelSupport() + } + } + + private suspend fun isRooted(): Boolean { + return try { + withContext(ioDispatcher) { + rootShell.get().start() + } + true + } catch (_: Exception) { + false + } + } } 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 a9f3e4e..be51b7b 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt @@ -1,9 +1,8 @@ package com.zaneschepke.wireguardautotunnel.ui +import android.content.Context import android.os.Bundle -import androidx.activity.SystemBarStyle import androidx.activity.compose.setContent -import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.compose.animation.core.tween @@ -11,8 +10,12 @@ import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.focusable import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.consumeWindowInsets 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.material.icons.Icons import androidx.compose.material.icons.rounded.Home import androidx.compose.material.icons.rounded.QuestionMark @@ -29,8 +32,6 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusProperties -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen @@ -41,6 +42,8 @@ import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import androidx.navigation.toRoute 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 @@ -48,8 +51,9 @@ 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.LocalNavController +import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar import com.zaneschepke.wireguardautotunnel.ui.common.navigation.isCurrentRoute -import com.zaneschepke.wireguardautotunnel.ui.common.prompt.CustomSnackBar +import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.CustomSnackBar import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarControllerProvider import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigScreen import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen @@ -57,10 +61,15 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.options.OptionsScreen import com.zaneschepke.wireguardautotunnel.ui.screens.pinlock.PinLockScreen import com.zaneschepke.wireguardautotunnel.ui.screens.scanner.ScannerScreen import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen +import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.AppearanceScreen +import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.display.DisplayScreen +import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.language.LanguageScreen +import com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.AutoTunnelScreen import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen import com.zaneschepke.wireguardautotunnel.ui.screens.support.logs.LogsScreen import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme import com.zaneschepke.wireguardautotunnel.util.Constants +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 @@ -68,6 +77,13 @@ import javax.inject.Inject @AndroidEntryPoint class MainActivity : AppCompatActivity() { + + private val localeStorage: LocaleStorage by lazy { + (application as WireGuardAutoTunnel).localeStorage + } + + private lateinit var oldPrefLocaleCode: String + @Inject lateinit var appStateRepository: AppStateRepository @@ -78,12 +94,6 @@ class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - enableEdgeToEdge( - navigationBarStyle = SystemBarStyle.auto( - lightScrim = Color.Transparent.toArgb(), - darkScrim = Color.Transparent.toArgb(), - ), - ) installSplashScreen().apply { setKeepOnScreenCondition { @@ -113,9 +123,10 @@ class MainActivity : AppCompatActivity() { CompositionLocalProvider(LocalNavController provides navController) { SnackbarControllerProvider { host -> - WireguardAutoTunnelTheme { + WireguardAutoTunnelTheme(theme = appUiState.generalState.theme){ val focusRequester = remember { FocusRequester() } Scaffold( + contentWindowInsets = WindowInsets(0.dp), snackbarHost = { SnackbarHost(host) { snackbarData: SnackbarData -> CustomSnackBar( @@ -160,8 +171,8 @@ class MainActivity : AppCompatActivity() { ), ) }, - ) { padding -> - Box(modifier = Modifier.fillMaxSize().padding(padding)) { + ) { + Box(modifier = Modifier.fillMaxSize().padding(it)) { NavHost( navController, enterTransition = { fadeIn(tween(Constants.TRANSITION_ANIMATION_TIME)) }, @@ -181,6 +192,20 @@ class MainActivity : AppCompatActivity() { focusRequester = focusRequester, ) } + composable { + AutoTunnelScreen( + appUiState, + ) + } + composable { + AppearanceScreen() + } + composable { + LanguageScreen(localeStorage) + } + composable { + DisplayScreen(appUiState) + } composable { SupportScreen( focusRequester = focusRequester, @@ -222,6 +247,21 @@ class MainActivity : AppCompatActivity() { } } + override fun attachBaseContext(newBase: Context) { + oldPrefLocaleCode = LocaleStorage(newBase).getPreferredLocale() + applyOverrideConfiguration(LocaleUtil.getLocalizedConfiguration(oldPrefLocaleCode)) + super.attachBaseContext(newBase) + } + + override fun onResume() { + val currentLocaleCode = LocaleStorage(this).getPreferredLocale() + if (oldPrefLocaleCode != currentLocaleCode) { + recreate() // locale is changed, restart the activity to update + oldPrefLocaleCode = currentLocaleCode + } + super.onResume() + } + override fun onDestroy() { super.onDestroy() tunnelService.cancelStatsJob() diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/Route.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/Route.kt index 8a3af84..0d0e7d3 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/Route.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/Route.kt @@ -9,6 +9,18 @@ sealed class Route { @Serializable data object Settings : Route() + @Serializable + data object AutoTunnel : Route() + + @Serializable + data object Appearance : Route() + + @Serializable + data object Display : Route() + + @Serializable + data object Language : Route() + @Serializable data object Main : Route() diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/SelectedLabel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/SelectedLabel.kt new file mode 100644 index 0000000..bf366d5 --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/SelectedLabel.kt @@ -0,0 +1,37 @@ +package com.zaneschepke.wireguardautotunnel.ui.common + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.zaneschepke.wireguardautotunnel.R +import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight +import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth + +@Composable +fun SelectedLabel() { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + stringResource(id = R.string.selected), + modifier = + Modifier.padding( + horizontal = 24.dp.scaledWidth(), + vertical = 16.dp.scaledHeight(), + ), + color = + MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.labelSmall, + ) + } +} 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 new file mode 100644 index 0000000..f12fede --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/IconSurfaceButton.kt @@ -0,0 +1,89 @@ +package com.zaneschepke.wireguardautotunnel.ui.common.button + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.unit.dp +import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize +import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight +import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth +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 + val interactionSource = + androidx.compose.runtime.remember { androidx.compose.foundation.interaction.MutableInteractionSource() } + androidx.compose.material3.Card( + modifier = + Modifier + .fillMaxWidth() + .height(IntrinsicSize.Min) + .clickable(interactionSource = interactionSource, indication = null) { + onClick() + }, + shape = androidx.compose.foundation.shape.RoundedCornerShape(8.dp), + border = border, + colors = androidx.compose.material3.CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), + ) { + Column( + modifier = + Modifier + .padding(horizontal = 8.dp.scaledWidth(), vertical = 10.dp.scaledHeight()) + .padding(end = 16.dp.scaledWidth()).padding(start = 8.dp.scaledWidth()) + .fillMaxSize(), + verticalArrangement = androidx.compose.foundation.layout.Arrangement.Center, + horizontalAlignment = androidx.compose.ui.Alignment.Companion.Start, + ) { + androidx.compose.foundation.layout.Row( + verticalAlignment = androidx.compose.ui.Alignment.Companion.CenterVertically, + horizontalArrangement = androidx.compose.foundation.layout.Arrangement.spacedBy(16.dp.scaledWidth()), + ) { + androidx.compose.foundation.layout.Row( + horizontalArrangement = androidx.compose.foundation.layout.Arrangement.spacedBy( + 16.dp.scaledWidth() + ), + verticalAlignment = androidx.compose.ui.Alignment.Companion.CenterVertically, + modifier = Modifier.padding(vertical = if (description == null) 10.dp.scaledHeight() else 0.dp), + ) { + leadingIcon?.let { + Icon( + leadingIcon, + leadingIcon.name, + Modifier.Companion.size(iconSize.scaledWidth()), + if (selected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface, + ) + } + Column { + Text( + title, + style = MaterialTheme.typography.titleMedium + ) + description?.let { + Text( + description, + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.bodyMedium, + ) + } + } + } + } + } + } +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/ScaledSwitch.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/ScaledSwitch.kt new file mode 100644 index 0000000..ec4c24e --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/ScaledSwitch.kt @@ -0,0 +1,18 @@ +package com.zaneschepke.wireguardautotunnel.ui.common.button + +import androidx.compose.material3.Switch +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.scale +import androidx.compose.ui.unit.dp +import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight + +@Composable +fun ScaledSwitch(checked: Boolean, onClick: (checked: Boolean) -> Unit, enabled: Boolean = true) { + Switch( + checked, + { onClick(it) }, + Modifier.scale((52.dp.scaledHeight() / 52.dp)), + enabled = enabled + ) +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/SelectionItemButton.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/SelectionItemButton.kt new file mode 100644 index 0000000..959a21a --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/SelectionItemButton.kt @@ -0,0 +1,63 @@ +package com.zaneschepke.wireguardautotunnel.ui.common.button + +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.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.ripple +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight + +@Composable +fun SelectionItemButton( + leading: (@Composable () -> Unit)? = null, + buttonText: String, + trailing: (@Composable () -> Unit)? = null, + onClick: () -> Unit, + ripple: Boolean = true, +) { + Card( + modifier = + Modifier.clip(RoundedCornerShape(8.dp)) + .clickable( + indication = if (ripple) ripple() else null, + interactionSource = remember { MutableInteractionSource() }, + onClick = { onClick() }, + ) + .height(56.dp.scaledHeight()), + colors = + CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.background, + ), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start, + modifier = Modifier.fillMaxSize(), + ) { + leading?.let { + it() + } + Text( + buttonText, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface, + ) + trailing?.let { + it() + } + } + } +} 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 new file mode 100644 index 0000000..f92eec4 --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/surface/SelectionItem.kt @@ -0,0 +1,13 @@ +package com.zaneschepke.wireguardautotunnel.ui.common.button.surface + +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.vector.ImageVector + +data class SelectionItem( + val leadingIcon: ImageVector? = null, + val trailing: (@Composable () -> Unit)? = null, + val title: (@Composable () -> Unit), + val description: (@Composable () -> Unit)? = null, + val onClick: (() -> Unit)? = null, + val height: Int = 64, +) 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 new file mode 100644 index 0000000..9f3fa47 --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/surface/SurfaceSelectionGroupButton.kt @@ -0,0 +1,93 @@ +package com.zaneschepke.wireguardautotunnel.ui.common.button.surface + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowRight +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize +import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight +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, it -> + Box( + contentAlignment = Alignment.Center, + modifier = (it.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 + .padding(start = 16.dp.scaledWidth()) + .weight(4f, false) + .fillMaxWidth(), + ) { + it.leadingIcon?.let { icon -> + Icon( + icon, + icon.name, + modifier = Modifier.size(iconSize.scaledWidth()), + ) + } + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterVertically), + modifier = Modifier + .fillMaxWidth() + .padding(start = if (it.leadingIcon != null) 16.dp.scaledWidth() else 0.dp) + .padding(vertical = if (it.description == null) 16.dp.scaledHeight() else 6.dp.scaledHeight()), + ) { + it.title() + it.description?.let { + it() + } + } + } + it.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) + } + } +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/config/ConfigurationToggle.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/config/ConfigurationToggle.kt index d5be376..761ebcc 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/config/ConfigurationToggle.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/config/ConfigurationToggle.kt @@ -3,7 +3,6 @@ package com.zaneschepke.wireguardautotunnel.ui.common.config import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Switch import androidx.compose.material3.Text @@ -11,22 +10,19 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.Dp @Composable fun ConfigurationToggle( label: String, enabled: Boolean = true, checked: Boolean, - padding: Dp, onCheckChanged: (checked: Boolean) -> Unit, modifier: Modifier = Modifier, ) { Row( modifier = Modifier - .fillMaxWidth() - .padding(padding), + .fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween, ) { diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/screen/LoadingScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/screen/LoadingScreen.kt deleted file mode 100644 index 04ea15d..0000000 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/screen/LoadingScreen.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.zaneschepke.wireguardautotunnel.ui.common.screen - -import androidx.compose.foundation.focusable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp - -@Composable -fun LoadingScreen() { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Top, - modifier = - Modifier - .fillMaxSize() - .focusable() - .padding(), - ) { - Column(modifier = Modifier.padding(120.dp)) { CircularProgressIndicator() } - } -} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/prompt/CustomSnackbar.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/snackbar/CustomSnackbar.kt similarity index 97% rename from app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/prompt/CustomSnackbar.kt rename to app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/snackbar/CustomSnackbar.kt index 6ddd3d8..8a797ee 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/prompt/CustomSnackbar.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/snackbar/CustomSnackbar.kt @@ -1,4 +1,4 @@ -package com.zaneschepke.wireguardautotunnel.ui.common.prompt +package com.zaneschepke.wireguardautotunnel.ui.common.snackbar import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.IntrinsicSize diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/textbox/CustomTextField.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/textbox/CustomTextField.kt new file mode 100644 index 0000000..16e7bd2 --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/textbox/CustomTextField.kt @@ -0,0 +1,110 @@ +package com.zaneschepke.wireguardautotunnel.ui.common.textbox + +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.unit.dp + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CustomTextField( + value: String, + modifier: Modifier = Modifier, + textStyle: TextStyle = MaterialTheme.typography.bodyLarge.copy(color = MaterialTheme.colorScheme.onSurface), + label: @Composable () -> Unit, + containerColor: Color, + onValueChange: (value: String) -> Unit = {}, + singleLine: Boolean = false, + placeholder: @Composable (() -> Unit)? = null, + keyboardOptions: KeyboardOptions, + keyboardActions: KeyboardActions, + supportingText: @Composable (() -> Unit)? = null, + leading: @Composable (() -> Unit)? = null, + trailing: @Composable (() -> Unit)? = null, + isError: Boolean = false, + readOnly: Boolean = false, + enabled: Boolean = true, +) { + val interactionSource = remember { MutableInteractionSource() } + val space = " " + BasicTextField( + value = value, + textStyle = textStyle, + onValueChange = { + onValueChange(it) + }, + keyboardActions = keyboardActions, + keyboardOptions = keyboardOptions, + readOnly = readOnly, + cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurface), + modifier = modifier, + interactionSource = interactionSource, + enabled = enabled, + singleLine = singleLine, + ) { + OutlinedTextFieldDefaults.DecorationBox( + value = space + value, + innerTextField = { + if (value.isEmpty()) { + if (placeholder != null) { + placeholder() + } + } + it.invoke() + }, + contentPadding = OutlinedTextFieldDefaults.contentPadding(top = 0.dp, bottom = 0.dp), + leadingIcon = leading, + trailingIcon = trailing, + singleLine = singleLine, + supportingText = supportingText, + colors = TextFieldDefaults.colors().copy( + disabledLabelColor = MaterialTheme.colorScheme.onSurface, + disabledContainerColor = containerColor, + focusedLabelColor = MaterialTheme.colorScheme.onSurface, + focusedContainerColor = containerColor, + unfocusedContainerColor = containerColor, + focusedTextColor = MaterialTheme.colorScheme.onSurface, + cursorColor = MaterialTheme.colorScheme.onSurface, + ), + enabled = enabled, + label = label, + visualTransformation = VisualTransformation.None, + interactionSource = interactionSource, + placeholder = placeholder, + container = { + OutlinedTextFieldDefaults.ContainerBox( + enabled, + isError = isError, + interactionSource, + colors = TextFieldDefaults.colors().copy( + errorContainerColor = containerColor, + disabledLabelColor = MaterialTheme.colorScheme.onSurface, + disabledContainerColor = containerColor, + focusedIndicatorColor = MaterialTheme.colorScheme.onSurface, + focusedLabelColor = MaterialTheme.colorScheme.onSurface, + focusedContainerColor = containerColor, + unfocusedContainerColor = containerColor, + focusedTextColor = MaterialTheme.colorScheme.onSurface, + cursorColor = MaterialTheme.colorScheme.onSurface, + ), + shape = RoundedCornerShape(8.dp), + focusedBorderThickness = 0.5.dp, + unfocusedBorderThickness = 0.5.dp, + ) + }, + ) + } +} 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 f933393..637038e 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 @@ -68,6 +68,7 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.config.components.Applicat import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigType import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv +import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight import kotlinx.coroutines.delay @Composable @@ -193,7 +194,7 @@ fun ConfigScreen(tunnelId: Int, focusRequester: FocusRequester) { } }, ) { - Column(Modifier.padding(it)) { + Column(Modifier.padding(top = 24.dp.scaledHeight()).padding(it)) { Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Top, @@ -235,7 +236,6 @@ fun ConfigScreen(tunnelId: Int, focusRequester: FocusRequester) { ConfigurationToggle( stringResource(id = R.string.show_amnezia_properties), checked = derivedConfigType.value == ConfigType.AMNEZIA, - padding = screenPadding, onCheckChanged = { configType = if (it) ConfigType.AMNEZIA else ConfigType.WIREGUARD }, modifier = Modifier.focusRequester(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 0464605..1282852 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 @@ -9,7 +9,10 @@ import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.gestures.ScrollableDefaults import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState @@ -146,7 +149,7 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState, selectedTunnel = null }, ) - }, + }.windowInsetsPadding(WindowInsets.systemBars), floatingActionButtonPosition = FabPosition.End, floatingActionButton = { ScrollDismissFab({ 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 81a179e..0a0bd25 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 @@ -163,7 +163,6 @@ fun OptionsScreen(optionsViewModel: OptionsViewModel = hiltViewModel(), focusReq modifier = Modifier .focusRequester(focusRequester), - padding = screenPadding, onCheckChanged = { optionsViewModel.onTogglePrimaryTunnel(config) }, ) } @@ -201,7 +200,6 @@ fun OptionsScreen(optionsViewModel: OptionsViewModel = hiltViewModel(), focusReq stringResource(R.string.mobile_data_tunnel), enabled = true, checked = config.isMobileDataTunnel, - padding = screenPadding, onCheckChanged = { optionsViewModel.onToggleIsMobileDataTunnel(config) }, ) Column { @@ -273,7 +271,6 @@ fun OptionsScreen(optionsViewModel: OptionsViewModel = hiltViewModel(), focusReq stringResource(R.string.restart_on_ping), enabled = !appUiState.settings.isPingEnabled, checked = config.isPingEnabled || appUiState.settings.isPingEnabled, - padding = screenPadding, onCheckChanged = { optionsViewModel.onToggleRestartOnPing(config) }, ) if (config.isPingEnabled || appUiState.settings.isPingEnabled) { 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 6e0d5a9..5373b14 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,11 +1,9 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.settings -import android.Manifest import android.content.Context.POWER_SERVICE import android.content.Intent import android.net.Uri import android.net.VpnService -import android.os.Build import android.os.PowerManager import android.provider.Settings import androidx.activity.compose.rememberLauncherForActivityResult @@ -17,31 +15,34 @@ 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.FlowRow -import androidx.compose.foundation.layout.IntrinsicSize -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.height 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.shape.RoundedCornerShape -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Close -import androidx.compose.material.icons.outlined.Add +import androidx.compose.material.icons.automirrored.outlined.ArrowForward +import androidx.compose.material.icons.automirrored.outlined.ArrowRight +import androidx.compose.material.icons.automirrored.outlined.ViewQuilt +import androidx.compose.material.icons.filled.AppShortcut +import androidx.compose.material.icons.filled.Bolt +import androidx.compose.material.icons.filled.Restore +import androidx.compose.material.icons.filled.VpnLock +import androidx.compose.material.icons.outlined.AdminPanelSettings +import androidx.compose.material.icons.outlined.AppShortcut +import androidx.compose.material.icons.outlined.Bolt +import androidx.compose.material.icons.outlined.Code +import androidx.compose.material.icons.outlined.FolderZip +import androidx.compose.material.icons.outlined.Notifications +import androidx.compose.material.icons.outlined.Pin +import androidx.compose.material.icons.outlined.Restore +import androidx.compose.material.icons.outlined.VpnLock import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField -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 @@ -49,39 +50,35 @@ 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.focus.focusRequester import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.google.accompanist.permissions.ExperimentalPermissionsApi -import com.google.accompanist.permissions.isGranted -import com.google.accompanist.permissions.rememberPermissionState import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState import com.zaneschepke.wireguardautotunnel.ui.AppUiState import com.zaneschepke.wireguardautotunnel.ui.AppViewModel import com.zaneschepke.wireguardautotunnel.ui.Route -import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton -import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle +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.SurfaceSelectionGroupButton 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.common.text.SectionTitle import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.VpnDeniedDialog import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.BackgroundLocationDialog import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.BackgroundLocationDisclosure import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.LocationServicesDialog -import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.WildcardSupportingLabel import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv import com.zaneschepke.wireguardautotunnel.util.extensions.launchAppSettings -import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl +import com.zaneschepke.wireguardautotunnel.util.extensions.launchNotificationSettings +import com.zaneschepke.wireguardautotunnel.util.extensions.launchVpnSettings +import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight +import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth import com.zaneschepke.wireguardautotunnel.util.extensions.showToast import xyz.teamgravity.pin_lock_compose.PinManager @@ -98,26 +95,14 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel: val scrollState = rememberScrollState() val interactionSource = remember { MutableInteractionSource() } - val isRunningOnTv = context.isRunningOnTv() val settingsUiState by viewModel.uiState.collectAsStateWithLifecycle() - val fineLocationState = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION) - var currentText by remember { mutableStateOf("") } - var isBackgroundLocationGranted by remember { mutableStateOf(true) } var showVpnPermissionDialog by remember { mutableStateOf(false) } var showLocationServicesAlertDialog by remember { mutableStateOf(false) } - val didExportFiles by remember { mutableStateOf(false) } var showAuthPrompt by remember { mutableStateOf(false) } var showLocationDialog by remember { mutableStateOf(false) } - val screenPadding = 5.dp - val fillMaxWidth = .85f - - LaunchedEffect(uiState.settings.trustedNetworkSSIDs) { - currentText = "" - } - val startForResult = rememberLauncherForActivityResult( ActivityResultContracts.StartActivityForResult(), @@ -171,44 +156,38 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel: viewModel.onToggleAutoTunnel(context) } - fun saveTrustedSSID() { - if (currentText.isNotEmpty()) { - viewModel.onSaveTrustedSSID(currentText) - } - } +// fun checkFineLocationGranted() { +// isBackgroundLocationGranted = +// if (!fineLocationState.status.isGranted) { +// false +// } else { +// viewModel.setLocationDisclosureShown() +// true +// } +// } - 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() - } +// 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() +// } if (!uiState.generalState.isLocationDisclosureShown) { BackgroundLocationDisclosure( onDismiss = { viewModel.setLocationDisclosureShown() }, @@ -240,7 +219,7 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel: AuthorizationPrompt( onSuccess = { showAuthPrompt = false - viewModel.exportAllConfigs() + viewModel.exportAllConfigs(context) }, onError = { _ -> showAuthPrompt = false @@ -257,351 +236,496 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel: ) } - fun onAutoTunnelWifiChecked() { - when (false) { - isBackgroundLocationGranted -> showLocationDialog = true - fineLocationState.status.isGranted -> showLocationDialog = true - viewModel.isLocationEnabled(context) -> - showLocationServicesAlertDialog = true - else -> { - viewModel.onToggleTunnelOnWifi() - } - } - } - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Top, + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.Top), modifier = Modifier + .verticalScroll(rememberScrollState()) .fillMaxSize() - .verticalScroll(scrollState) - .clickable( + .padding(top = 24.dp.scaledHeight()) + .padding(horizontal = 24.dp.scaledWidth()).clickable( indication = null, interactionSource = interactionSource, ) { focusManager.clearFocus() - }, + }.windowInsetsPadding(WindowInsets.systemBars), ) { - 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, - padding = screenPadding, - onCheckChanged = { checked -> - if (!checked || settingsUiState.isRooted) viewModel.onToggleTunnelOnWifi().also { return@ConfigurationToggle } - onAutoTunnelWifiChecked() + SurfaceSelectionGroupButton( + listOf( + SelectionItem( + Icons.Outlined.Bolt, + title = { Text(stringResource(R.string.auto_tunneling), style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface)) }, + description = { + Text( + "Configure on demand tunnel rules", + style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.outline), + ) }, - modifier = - if (uiState.settings.isAutoTunnelEnabled) { - Modifier - } else { - Modifier - .focusRequester(focusRequester) + onClick = { + navController.navigate(Route.AutoTunnel) }, - ) - 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, + trailing = { + val icon = Icons.AutoMirrored.Outlined.ArrowForward + Icon(icon, icon.name) + } + ), + ), + ) + + SurfaceSelectionGroupButton( + listOf( + SelectionItem( + Icons.Filled.AppShortcut, + { + ScaledSwitch( + uiState.settings.isShortcutsEnabled, + onClick = { viewModel.onToggleShortcutsEnabled() }, + ) + }, + title = { + Text(stringResource(R.string.enabled_app_shortcuts), style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface)) + }, + ), + SelectionItem( + Icons.Outlined.VpnLock, + { + ScaledSwitch( + enabled = !( + ( + uiState.settings.isTunnelOnWifiEnabled || + uiState.settings.isTunnelOnEthernetEnabled || + uiState.settings.isTunnelOnMobileDataEnabled + ) && + uiState.settings.isAutoTunnelEnabled ), - 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, - ) - } + onClick = { viewModel.onToggleAlwaysOnVPN() }, + checked = uiState.settings.isAlwaysOnVpnEnabled, + ) + }, + title = { + Text(stringResource(R.string.always_on_vpn_support), style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface)) + }, + ), + SelectionItem( + Icons.Outlined.AdminPanelSettings, + title = { Text(stringResource(R.string.kill_switch), style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface)) }, + onClick = { + context.launchVpnSettings() + }, + trailing = { + val icon = Icons.AutoMirrored.Outlined.ArrowForward + Icon(icon, icon.name) + } + ), + SelectionItem( + Icons.Outlined.Restore, + { + ScaledSwitch( + uiState.settings.isRestoreOnBootEnabled, + onClick = { viewModel.onToggleRestartAtBoot() }, + ) + }, + title = { Text(stringResource(R.string.restart_at_boot), style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface)) }, + ), + ), + ) + + SurfaceSelectionGroupButton( + mutableListOf( + SelectionItem( + Icons.AutoMirrored.Outlined.ViewQuilt, + title = { Text(stringResource(R.string.appearance), style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface)) }, + onClick = { + navController.navigate(Route.Appearance) + }, + trailing = { + val icon = Icons.AutoMirrored.Outlined.ArrowForward + Icon(icon, icon.name) + } + ), + SelectionItem( + Icons.Outlined.Notifications, + title = { Text(stringResource(R.string.notifications), style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface)) }, + onClick = { + context.launchNotificationSettings() + }, + trailing = { + val icon = Icons.AutoMirrored.Outlined.ArrowForward + Icon(icon, icon.name) + } + ), + SelectionItem( + Icons.Outlined.Pin, + title = { Text(stringResource(R.string.enable_app_lock), style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface)) }, + trailing = { + ScaledSwitch( + uiState.generalState.isPinLockEnabled, + onClick = { + if (uiState.generalState.isPinLockEnabled) { + appViewModel.onPinLockDisabled() + } else { + PinManager.initialize(context) + navController.navigate(Route.Lock) } }, ) } - } - ConfigurationToggle( - stringResource(R.string.tunnel_mobile_data), - enabled = !uiState.settings.isAlwaysOnVpnEnabled, - checked = uiState.settings.isTunnelOnMobileDataEnabled, - padding = screenPadding, - onCheckChanged = { viewModel.onToggleTunnelOnMobileData() }, - ) - ConfigurationToggle( - stringResource(id = R.string.tunnel_on_ethernet), - enabled = !uiState.settings.isAlwaysOnVpnEnabled, - checked = uiState.settings.isTunnelOnEthernetEnabled, - padding = screenPadding, - onCheckChanged = { viewModel.onToggleTunnelOnEthernet() }, - ) - ConfigurationToggle( - stringResource(R.string.restart_on_ping), - checked = uiState.settings.isPingEnabled, - padding = screenPadding, - onCheckChanged = { viewModel.onToggleRestartOnPing() }, - ) - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = - ( - if (!uiState.settings.isAutoTunnelEnabled) { - Modifier - } else { - Modifier.focusRequester( - focusRequester, - ) - } + ), + ), + ) + + SurfaceSelectionGroupButton( + listOf( + SelectionItem( + Icons.Outlined.Code, + title = { Text(stringResource(R.string.kernel), style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface)) }, + description = { + Text( + "Use kernel backend (root only)", + style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.outline), ) - .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, - padding = screenPadding, - 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, - padding = screenPadding, - onCheckChanged = { viewModel.onToggleAlwaysOnVPN() }, - ) - ConfigurationToggle( - stringResource(R.string.enabled_app_shortcuts), - enabled = true, - checked = uiState.settings.isShortcutsEnabled, - padding = screenPadding, - onCheckChanged = { viewModel.onToggleShortcutsEnabled() }, - ) - } - ConfigurationToggle( - stringResource(R.string.restart_at_boot), - enabled = true, - checked = uiState.settings.isRestoreOnBootEnabled, - padding = screenPadding, - onCheckChanged = { - viewModel.onToggleRestartAtBoot() + trailing = { + ScaledSwitch( + uiState.settings.isKernelEnabled, + onClick = { viewModel.onToggleKernelMode() }, + enabled = !( + uiState.settings.isAutoTunnelEnabled || + uiState.settings.isAlwaysOnVpnEnabled || + (uiState.vpnState.status == TunnelState.UP) || + !settingsUiState.isKernelAvailable + ), + ) }, - ) - ConfigurationToggle( - stringResource(R.string.enable_app_lock), - enabled = true, - checked = uiState.generalState.isPinLockEnabled, - padding = screenPadding, - 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) - } + ), + ), + ) + + SurfaceSelectionGroupButton( + listOf( + SelectionItem( + Icons.Outlined.FolderZip, + title = { Text(stringResource(R.string.export_configs), style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface)) }, + onClick = { + if (uiState.tunnels.isEmpty()) return@SelectionItem context.showToast(R.string.tunnel_required) + showAuthPrompt = true }, - ) - 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)) - } - } - } - } - } + trailing = {}, + ), + ), + ) + +// 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)) +// } +// } +// } +// } +// } } } 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 6460095..992a86a 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 @@ -1,8 +1,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.settings import android.content.Context -import android.location.LocationManager -import androidx.core.location.LocationManagerCompat +import androidx.core.content.FileProvider import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.wireguard.android.backend.WgQuickBackend @@ -16,6 +15,7 @@ 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 @@ -25,7 +25,7 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import java.io.File +import java.time.Instant import javax.inject.Inject import javax.inject.Provider @@ -52,22 +52,6 @@ constructor( private val settings = appDataRepository.settings.getSettingsFlow() .stateIn(viewModelScope, SharingStarted.Eagerly, Settings()) - fun onSaveTrustedSSID(ssid: String) = viewModelScope.launch { - val trimmed = ssid.trim() - with(settings.value) { - if (!trustedNetworkSSIDs.contains(trimmed)) { - this.trustedNetworkSSIDs.add(ssid) - appDataRepository.settings.save(this) - } else { - SnackbarController.showMessage( - StringValue.StringResource( - R.string.error_ssid_exists, - ), - ) - } - } - } - fun setLocationDisclosureShown() = viewModelScope.launch { appDataRepository.appState.setLocationDisclosureShown(true) } @@ -76,34 +60,6 @@ constructor( appDataRepository.appState.setBatteryOptimizationDisableShown(true) } - fun onToggleTunnelOnMobileData() = viewModelScope.launch { - with(settings.value) { - appDataRepository.settings.save( - copy( - isTunnelOnMobileDataEnabled = !this.isTunnelOnMobileDataEnabled, - ), - ) - } - } - - fun onDeleteTrustedSSID(ssid: String) = viewModelScope.launch { - with(settings.value) { - appDataRepository.settings.save( - copy( - trustedNetworkSSIDs = (this.trustedNetworkSSIDs - ssid).toMutableList(), - ), - ) - } - } - - private fun exportTunnels(files: List) = viewModelScope.launch { - fileUtils.saveFilesToZip(files).onSuccess { - SnackbarController.showMessage(StringValue.StringResource(R.string.exported_configs_message)) - }.onFailure { - SnackbarController.showMessage(StringValue.StringResource(R.string.export_configs_failed)) - } - } - fun onToggleAutoTunnel(context: Context) = viewModelScope.launch { with(settings.value) { var isAutoTunnelPaused = this.isAutoTunnelPaused @@ -132,24 +88,6 @@ constructor( } } - fun onToggleTunnelOnEthernet() = viewModelScope.launch { - with(settings.value) { - appDataRepository.settings.save( - copy( - isTunnelOnEthernetEnabled = !isTunnelOnEthernetEnabled, - ), - ) - } - } - - fun isLocationEnabled(context: Context): Boolean { - val locationManager = - context.getSystemService( - Context.LOCATION_SERVICE, - ) as LocationManager - return LocationManagerCompat.isLocationEnabled(locationManager) - } - fun onToggleShortcutsEnabled() = viewModelScope.launch { with(settings.value) { appDataRepository.settings.save( @@ -170,16 +108,6 @@ constructor( } } - fun onToggleTunnelOnWifi() = viewModelScope.launch { - with(settings.value) { - appDataRepository.settings.save( - copy( - isTunnelOnWifiEnabled = !isTunnelOnWifiEnabled, - ), - ) - } - } - fun onToggleAmnezia() = viewModelScope.launch { with(settings.value) { if (isKernelEnabled) { @@ -210,16 +138,6 @@ constructor( } } - fun onToggleRestartOnPing() = viewModelScope.launch { - with(settings.value) { - appDataRepository.settings.save( - copy( - isPingEnabled = !isPingEnabled, - ), - ) - } - } - private suspend fun isKernelSupported(): Boolean { return withContext(ioDispatcher) { WgQuickBackend.hasKernelSupport() @@ -262,12 +180,16 @@ constructor( requestRoot() } - fun exportAllConfigs() = viewModelScope.launch { + fun exportAllConfigs(context: Context) = viewModelScope.launch { kotlin.runCatching { + val shareFile = fileUtils.createNewShareFile("wg-export_${Instant.now().epochSecond}.zip") val tunnels = appDataRepository.tunnels.getAll() val wgFiles = fileUtils.createWgFiles(tunnels) val amFiles = fileUtils.createAmFiles(tunnels) - exportTunnels(wgFiles + amFiles) + val allFiles = wgFiles + amFiles + fileUtils.zipAll(shareFile, allFiles) + val uri = FileProvider.getUriForFile(context, context.getString(R.string.provider), shareFile) + context.launchShareFile(uri) } } } 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 new file mode 100644 index 0000000..7a751e4 --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/appearance/AppearanceScreen.kt @@ -0,0 +1,71 @@ +package com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance + +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.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.ArrowForward +import androidx.compose.material.icons.outlined.Contrast +import androidx.compose.material.icons.outlined.Translate +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.zaneschepke.wireguardautotunnel.ui.Route +import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem +import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton +import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController +import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight +import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth +import com.zaneschepke.wireguardautotunnel.R + +@Composable +fun AppearanceScreen() { + val navController = LocalNavController.current + + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top), + modifier = + Modifier + .fillMaxSize() + .windowInsetsPadding(WindowInsets.systemBars) + .padding(top = 24.dp.scaledHeight()) + .padding(horizontal = 24.dp.scaledWidth()), + ) { + SurfaceSelectionGroupButton( + listOf( + SelectionItem( + Icons.Outlined.Translate, + title = { Text(stringResource(R.string.language), style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface)) }, + onClick = { navController.navigate(Route.Language) }, + trailing = { + val icon = Icons.AutoMirrored.Outlined.ArrowForward + Icon(icon, icon.name) + } + ), + ), + ) + SurfaceSelectionGroupButton( + listOf( + SelectionItem( + Icons.Outlined.Contrast, + title = { Text(stringResource(R.string.display_theme), style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface)) }, + onClick = { navController.navigate(Route.Display) }, + trailing = { + val icon = Icons.AutoMirrored.Outlined.ArrowForward + Icon(icon, icon.name) + } + ), + ), + ) + } +} 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 new file mode 100644 index 0000000..be76f2c --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/appearance/display/DisplayScreen.kt @@ -0,0 +1,59 @@ +package com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.display + +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.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.zaneschepke.wireguardautotunnel.R +import com.zaneschepke.wireguardautotunnel.ui.AppUiState +import com.zaneschepke.wireguardautotunnel.ui.common.button.IconSurfaceButton +import com.zaneschepke.wireguardautotunnel.ui.theme.Theme +import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight +import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth + +@Composable +fun DisplayScreen(appUiState: AppUiState, viewModel: DisplayViewModel = hiltViewModel()) { + + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top), + modifier = + Modifier + .fillMaxSize() + .windowInsetsPadding(WindowInsets.systemBars) + .padding(top = 24.dp.scaledHeight()) + .padding(horizontal = 24.dp.scaledWidth()), + ) { + IconSurfaceButton( + title = stringResource(R.string.automatic), + onClick = { + viewModel.onThemeChange(Theme.AUTOMATIC) + }, + selected = appUiState.generalState.theme == Theme.AUTOMATIC, + ) + IconSurfaceButton( + title = stringResource(R.string.light), + onClick = { viewModel.onThemeChange(Theme.LIGHT) }, + selected = appUiState.generalState.theme == Theme.LIGHT, + ) + IconSurfaceButton( + title = stringResource(R.string.dark), + onClick = { viewModel.onThemeChange(Theme.DARK) }, + selected = appUiState.generalState.theme == Theme.DARK, + ) + IconSurfaceButton( + title = stringResource(R.string.dynamic), + onClick = { viewModel.onThemeChange(Theme.DYNAMIC) }, + selected = appUiState.generalState.theme == Theme.DYNAMIC, + ) + } +} 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 new file mode 100644 index 0000000..c8714c1 --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/appearance/display/DisplayViewModel.kt @@ -0,0 +1,21 @@ +package com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.display + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.zaneschepke.wireguardautotunnel.data.repository.AppStateRepository +import com.zaneschepke.wireguardautotunnel.ui.theme.Theme +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class DisplayViewModel +@Inject +constructor( + private val appStateRepository: AppStateRepository +) : ViewModel() { + + fun onThemeChange(theme: Theme) = viewModelScope.launch { + appStateRepository.setTheme(theme) + } +} 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 new file mode 100644 index 0000000..8b97f15 --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/appearance/language/LanguageScreen.kt @@ -0,0 +1,106 @@ +package com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.language + +import androidx.compose.foundation.layout.Arrangement +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 +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +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.unit.dp +import com.zaneschepke.wireguardautotunnel.R +import com.zaneschepke.wireguardautotunnel.data.datastore.LocaleStorage +import com.zaneschepke.wireguardautotunnel.ui.Route +import com.zaneschepke.wireguardautotunnel.ui.common.SelectedLabel +import com.zaneschepke.wireguardautotunnel.ui.common.button.SelectionItemButton +import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController +import com.zaneschepke.wireguardautotunnel.util.LocaleUtil +import com.zaneschepke.wireguardautotunnel.util.extensions.navigateAndForget +import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight +import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth +import timber.log.Timber +import java.text.Collator +import java.util.Locale + +@Composable +fun LanguageScreen(localeStorage: LocaleStorage) { + val navController = LocalNavController.current + + val context = LocalContext.current + + val collator = Collator.getInstance(Locale.getDefault()) + + val currentLocale = remember { mutableStateOf(LocaleUtil.OPTION_PHONE_LANGUAGE) } + + val locales = LocaleUtil.supportedLocales.map { + val tag = it.replace("_", "-") + Locale.forLanguageTag(tag) + } + + val sortedLocales = + remember(locales) { + locales.sortedWith(compareBy(collator) { it.getDisplayName(it) }).toList() + } + + LaunchedEffect(Unit) { + currentLocale.value = localeStorage.getPreferredLocale() + } + + fun onChangeLocale(locale: String) { + Timber.d("Setting preferred locale: $locale") + localeStorage.setPreferredLocale(locale) + LocaleUtil.applyLocalizedContext(context, locale) + navController.navigateAndForget(Route.Main) + } + + LazyColumn( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Top, + modifier = + Modifier + .fillMaxSize() + .windowInsetsPadding(WindowInsets.systemBars) + .padding(top = 24.dp.scaledHeight()) + .padding(horizontal = 24.dp.scaledWidth()).windowInsetsPadding(WindowInsets.navigationBars), + ) { + item { + SelectionItemButton( + buttonText = stringResource(R.string.automatic), + onClick = { + onChangeLocale(LocaleUtil.OPTION_PHONE_LANGUAGE) + }, + trailing = { + if (currentLocale.value == LocaleUtil.OPTION_PHONE_LANGUAGE) { + SelectedLabel() + } + }, + ripple = false, + ) + } + items(sortedLocales, key = { it }) { locale -> + SelectionItemButton( + buttonText = locale.getDisplayLanguage(locale).capitalize(locale) + + if (locale.toLanguageTag().contains("-")) " (${locale.getDisplayCountry(locale).capitalize(locale)})" else "", + onClick = { + onChangeLocale(locale.toLanguageTag()) + }, + trailing = { + if (locale.toLanguageTag() == currentLocale.value) { + SelectedLabel() + } + }, + ripple = false, + ) + } + } +} 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 new file mode 100644 index 0000000..cbe9b99 --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/autotunnel/AutoTunnelScreen.kt @@ -0,0 +1,232 @@ +package com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel + +import android.Manifest +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +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.ime +import androidx.compose.foundation.layout.isImeVisible +import androidx.compose.foundation.layout.navigationBars +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.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.outlined.Add +import androidx.compose.material.icons.outlined.NetworkPing +import androidx.compose.material.icons.outlined.Security +import androidx.compose.material.icons.outlined.SettingsEthernet +import androidx.compose.material.icons.outlined.SignalCellular4Bar +import androidx.compose.material.icons.outlined.Wifi +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.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.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.isGranted +import com.google.accompanist.permissions.rememberPermissionState +import com.zaneschepke.wireguardautotunnel.R +import com.zaneschepke.wireguardautotunnel.ui.AppUiState +import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton +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.SurfaceSelectionGroupButton +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.components.WildcardSupportingLabel +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 + +@OptIn(ExperimentalPermissionsApi::class, ExperimentalLayoutApi::class) +@Composable +fun AutoTunnelScreen(uiState: AppUiState, viewModel: AutoTunnelViewModel = hiltViewModel()) { + val context = LocalContext.current + + val fineLocationState = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION) + var currentText by remember { mutableStateOf("") } + var isBackgroundLocationGranted by remember { mutableStateOf(true) } + var showLocationServicesAlertDialog by remember { mutableStateOf(false) } + var showLocationDialog by remember { mutableStateOf(false) } + + LaunchedEffect(uiState.settings.trustedNetworkSSIDs) { + currentText = "" + } + + fun onAutoTunnelWifiChecked() { + when (false) { + isBackgroundLocationGranted -> showLocationDialog = true + fineLocationState.status.isGranted -> showLocationDialog = true + context.isLocationServicesEnabled() -> + showLocationServicesAlertDialog = true + else -> { + viewModel.onToggleTunnelOnWifi() + } + } + } + + Scaffold( + topBar = { + TopNavBar(stringResource(R.string.auto_tunneling)) + }, + ) { + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.Top), + modifier = + Modifier + .verticalScroll(rememberScrollState()) + .fillMaxSize() + .padding(top = 24.dp.scaledHeight()).padding(it) + .padding(horizontal = 24.dp.scaledWidth()), + ) { + SurfaceSelectionGroupButton( + mutableListOf( + SelectionItem( + Icons.Outlined.Wifi, + title = { Text(stringResource(R.string.tunnel_on_wifi), style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface)) }, + description = { + }, + trailing = { + ScaledSwitch( + enabled = !uiState.settings.isAlwaysOnVpnEnabled, + checked = uiState.settings.isTunnelOnWifiEnabled, + onClick = { checked -> + if (!checked || uiState.isRooted) viewModel.onToggleTunnelOnWifi().also { return@ScaledSwitch } + onAutoTunnelWifiChecked() + }, + ) + }, + ), + SelectionItem( + Icons.Outlined.SignalCellular4Bar, + title = { + Text( + stringResource(R.string.tunnel_mobile_data), + style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface), + ) + }, + trailing = { + ScaledSwitch( + enabled = !uiState.settings.isAlwaysOnVpnEnabled, + checked = uiState.settings.isTunnelOnMobileDataEnabled, + onClick = { viewModel.onToggleTunnelOnMobileData() }, + ) + }, + ), + SelectionItem( + Icons.Outlined.SettingsEthernet, + title = { + Text( + stringResource(R.string.tunnel_on_ethernet), + style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface), + ) + }, + trailing = { + ScaledSwitch( + enabled = !uiState.settings.isAlwaysOnVpnEnabled, + checked = uiState.settings.isTunnelOnEthernetEnabled, + onClick = { viewModel.onToggleTunnelOnEthernet() }, + ) + }, + ), + SelectionItem( + Icons.Outlined.NetworkPing, + title = { + Text( + stringResource(R.string.restart_on_ping), + style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface), + ) + }, + trailing = { + ScaledSwitch( + checked = uiState.settings.isPingEnabled, + onClick = { viewModel.onToggleRestartOnPing() }, + ) + }, + ), + ).apply { + if (uiState.settings.isTunnelOnWifiEnabled) { + add(1, + SelectionItem( + title = { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp.scaledHeight()), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .weight(4f, false) + .fillMaxWidth(), + ) { + val icon = Icons.Outlined.Security + Icon( + icon, + icon.name, + modifier = Modifier.size(iconSize.scaledWidth()), + ) + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterVertically), + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp.scaledWidth()) + .padding(vertical = 6.dp.scaledHeight()), + ) { + Text( + "Trusted wifi names", + style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface), + ) + } + + } + } + + }, + description = { + TrustedNetworkTextBox( + uiState.settings.trustedNetworkSSIDs, onDelete = viewModel::onDeleteTrustedSSID, + currentText = currentText, + onSave = viewModel::onSaveTrustedSSID, + onValueChange = { currentText = it } + ) + }, + ) + ) + } + }, + ) + } + } +} 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 new file mode 100644 index 0000000..565fa20 --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/autotunnel/AutoTunnelViewModel.kt @@ -0,0 +1,93 @@ +package com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.zaneschepke.wireguardautotunnel.R +import com.zaneschepke.wireguardautotunnel.data.domain.Settings +import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository +import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController +import com.zaneschepke.wireguardautotunnel.util.StringValue +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class AutoTunnelViewModel +@Inject +constructor( + private val appDataRepository: AppDataRepository, + +) : ViewModel() { + + private val settings = appDataRepository.settings.getSettingsFlow() + .stateIn(viewModelScope, SharingStarted.Eagerly, Settings()) + + fun onToggleTunnelOnWifi() = viewModelScope.launch { + with(settings.value) { + appDataRepository.settings.save( + copy( + isTunnelOnWifiEnabled = !isTunnelOnWifiEnabled, + ), + ) + } + } + + fun onToggleTunnelOnMobileData() = viewModelScope.launch { + with(settings.value) { + appDataRepository.settings.save( + copy( + isTunnelOnMobileDataEnabled = !this.isTunnelOnMobileDataEnabled, + ), + ) + } + } + + fun onDeleteTrustedSSID(ssid: String) = viewModelScope.launch { + with(settings.value) { + appDataRepository.settings.save( + copy( + trustedNetworkSSIDs = (this.trustedNetworkSSIDs - ssid).toMutableList(), + ), + ) + } + } + + fun onToggleTunnelOnEthernet() = viewModelScope.launch { + with(settings.value) { + appDataRepository.settings.save( + copy( + isTunnelOnEthernetEnabled = !isTunnelOnEthernetEnabled, + ), + ) + } + } + + fun onSaveTrustedSSID(ssid: String) = viewModelScope.launch { + if (ssid.isEmpty()) return@launch + val trimmed = ssid.trim() + with(settings.value) { + if (!trustedNetworkSSIDs.contains(trimmed)) { + this.trustedNetworkSSIDs.add(ssid) + appDataRepository.settings.save(this) + } else { + SnackbarController.showMessage( + StringValue.StringResource( + R.string.error_ssid_exists, + ), + ) + } + } + } + + fun onToggleRestartOnPing() = viewModelScope.launch { + with(settings.value) { + appDataRepository.settings.save( + copy( + isPingEnabled = !isPingEnabled, + ), + ) + } + } +} 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 new file mode 100644 index 0000000..1ac22fb --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/autotunnel/components/TrustNetworksTextBox.kt @@ -0,0 +1,100 @@ +package com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.outlined.Add +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.unit.dp +import com.zaneschepke.wireguardautotunnel.R +import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton +import com.zaneschepke.wireguardautotunnel.ui.common.textbox.CustomTextField +import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.WildcardSupportingLabel +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 + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun TrustedNetworkTextBox(trustedNetworks: List, onDelete: (ssid: String) -> Unit, currentText: String, onSave : (ssid: String) -> Unit, onValueChange: (network: String) -> Unit) { + val context = LocalContext.current + Column(verticalArrangement = Arrangement.spacedBy(10.dp.scaledHeight())){ + FlowRow( + modifier = + Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(5.dp), + ) { + trustedNetworks.forEach { ssid -> + ClickableIconButton( + onClick = { + if (context.isRunningOnTv()) { + //focusRequester.requestFocus() + onDelete(ssid) + } + }, + onIconClick = { + //if (context.isRunningOnTv()) focusRequester.requestFocus() + onDelete(ssid) + }, + text = ssid, + icon = Icons.Filled.Close, + ) + } + } + CustomTextField( + value = currentText, + onValueChange = onValueChange, + label = { Text(stringResource(R.string.add_trusted_ssid)) }, + containerColor = MaterialTheme.colorScheme.surface, + modifier = + Modifier + .padding( + top = 5.dp, + bottom = 10.dp, + ).fillMaxWidth().padding(end = 16.dp.scaledWidth()), + supportingText = { WildcardSupportingLabel { context.openWebUrl(it)} }, + singleLine = true, + keyboardOptions = + KeyboardOptions( + capitalization = KeyboardCapitalization.None, + imeAction = ImeAction.Done, + ), + keyboardActions = KeyboardActions(onDone = { onSave(currentText) }), + trailing = { + if (currentText != "") { + IconButton(onClick = { + onSave(currentText) + }) { + val icon = Icons.Outlined.Add + Icon( + imageVector = icon, + contentDescription = stringResource( + R.string + .trusted_ssid_value_description, + ), + tint = MaterialTheme.colorScheme.primary, + ) + } + } + }, + + ) + } +} 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 caaadf4..ceb885a 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 @@ -7,11 +7,14 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll @@ -63,6 +66,7 @@ fun SupportScreen(focusRequester: FocusRequester, appUiState: AppUiState) { modifier = Modifier .fillMaxSize() + .windowInsetsPadding(WindowInsets.systemBars) .verticalScroll(rememberScrollState()) .focusable(), ) { 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 918826a..96c7f40 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,8 +4,11 @@ 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/screens/support/logs/LogsViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/logs/LogsViewModel.kt index bac7515..ff2233f 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/logs/LogsViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/logs/LogsViewModel.kt @@ -11,6 +11,7 @@ import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.module.IoDispatcher import com.zaneschepke.wireguardautotunnel.module.MainDispatcher import com.zaneschepke.wireguardautotunnel.util.Constants +import com.zaneschepke.wireguardautotunnel.util.FileUtils import com.zaneschepke.wireguardautotunnel.util.extensions.chunked import com.zaneschepke.wireguardautotunnel.util.extensions.launchShareFile import dagger.hilt.android.lifecycle.HiltViewModel @@ -19,7 +20,6 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import timber.log.Timber -import java.io.File import java.time.Duration import java.time.Instant import javax.inject.Inject @@ -29,6 +29,7 @@ class LogsViewModel @Inject constructor( private val localLogCollector: LogReader, + private val fileUtils: FileUtils, @IoDispatcher private val ioDispatcher: CoroutineDispatcher, @MainDispatcher private val mainDispatcher: CoroutineDispatcher, ) : ViewModel() { @@ -51,12 +52,7 @@ constructor( fun shareLogs(context: Context): Job = viewModelScope.launch(ioDispatcher) { runCatching { - val sharePath = File(context.filesDir, "external_files") - if (sharePath.exists()) sharePath.delete() - sharePath.mkdir() - val file = File("${sharePath.path + "/" + Constants.BASE_LOG_FILE_NAME}-${Instant.now().epochSecond}.zip") - if (file.exists()) file.delete() - file.createNewFile() + val file = fileUtils.createNewShareFile("${Constants.BASE_LOG_FILE_NAME}-${Instant.now().epochSecond}.zip") localLogCollector.zipLogFiles(file.absolutePath) val uri = FileProvider.getUriForFile(context, context.getString(R.string.provider), file) context.launchShareFile(uri) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/Size.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/Size.kt new file mode 100644 index 0000000..21276f5 --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/Size.kt @@ -0,0 +1,6 @@ +package com.zaneschepke.wireguardautotunnel.ui.theme + +import androidx.compose.ui.unit.dp +import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight + +val iconSize = 24.dp.scaledHeight() 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 4d9072b..8c4eb3c 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 @@ -36,24 +36,34 @@ private val LightColorScheme = onSecondaryContainer = ThemeColors.Light.primary, ) +enum class Theme { + AUTOMATIC, + LIGHT, + DARK, + DYNAMIC +} + @Composable fun WireguardAutoTunnelTheme( - // force dark theme - useDarkTheme: Boolean = isSystemInDarkTheme(), + theme: Theme = Theme.AUTOMATIC, content: @Composable () -> Unit, ) { val context = LocalContext.current - val colorScheme = when { - (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) -> { - if (useDarkTheme) { - dynamicDarkColorScheme(context) - } else { - dynamicLightColorScheme(context) - } + val isDark = isSystemInDarkTheme() + val autoTheme = if(isDark) DarkColorScheme else LightColorScheme + val colorScheme = when(theme) { + Theme.AUTOMATIC -> autoTheme + Theme.DARK -> DarkColorScheme + Theme.LIGHT -> LightColorScheme + Theme.DYNAMIC -> { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + if (isDark) { + dynamicDarkColorScheme(context) + } else { + dynamicLightColorScheme(context) + } + } else autoTheme } - useDarkTheme -> DarkColorScheme - // TODO force dark theme for now until light theme designed - else -> DarkColorScheme } val view = LocalView.current if (!view.isInEditMode) { @@ -62,8 +72,7 @@ fun WireguardAutoTunnelTheme( WindowCompat.setDecorFitsSystemWindows(window, false) window.statusBarColor = Color.Transparent.toArgb() window.navigationBarColor = Color.Transparent.toArgb() - WindowCompat.getInsetsController(window, window.decorView).isAppearanceLightStatusBars = - !useDarkTheme + WindowCompat.getInsetsController(window, window.decorView).isAppearanceLightStatusBars = isDark } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/Type.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/Type.kt index 3a40321..5bc690d 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/Type.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/Type.kt @@ -5,9 +5,9 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.zaneschepke.wireguardautotunnel.R +import com.zaneschepke.wireguardautotunnel.util.extensions.scaled // Set of Material typography styles to start with @@ -17,43 +17,45 @@ val inter = FontFamily( val Typography = Typography( - bodyLarge = - TextStyle( - fontFamily = inter, + bodyLarge = TextStyle( fontWeight = FontWeight.Normal, - fontSize = 16.sp, - lineHeight = 24.sp, + fontSize = 16.sp.scaled(), + lineHeight = 24.sp.scaled(), letterSpacing = 0.5.sp, ), bodySmall = TextStyle( fontFamily = inter, fontWeight = FontWeight.Normal, - fontSize = 13.sp, - lineHeight = 20.sp, + fontSize = 13.sp.scaled(), + lineHeight = 20.sp.scaled(), letterSpacing = 1.sp, color = LightGrey, ), + bodyMedium = TextStyle( + fontSize = 14.sp.scaled(), + lineHeight = 20.sp.scaled(), + fontWeight = FontWeight(400), + letterSpacing = 0.25.sp, + ), labelLarge = TextStyle( fontFamily = inter, fontWeight = FontWeight.Normal, - fontSize = 15.sp, - lineHeight = 18.sp, + fontSize = 15.sp.scaled(), + lineHeight = 18.sp.scaled(), letterSpacing = 0.sp, ), labelMedium = TextStyle( fontFamily = inter, fontWeight = FontWeight.SemiBold, - fontSize = 12.sp, - lineHeight = 16.sp, + fontSize = 12.sp.scaled(), + lineHeight = 16.sp.scaled(), letterSpacing = 0.5.sp, ), titleMedium = TextStyle( fontFamily = inter, fontWeight = FontWeight.Bold, - fontSize = 17.sp, - lineHeight = 21.sp, + fontSize = 17.sp.scaled(), + lineHeight = 21.sp.scaled(), letterSpacing = 0.sp, ), ) - -val iconSize = 15.dp diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/FileUtils.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/FileUtils.kt index 3bf37ef..c74d104 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/FileUtils.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/FileUtils.kt @@ -1,19 +1,13 @@ package com.zaneschepke.wireguardautotunnel.util -import android.content.ContentValues import android.content.Context -import android.os.Build -import android.os.Environment -import android.provider.MediaStore -import android.provider.MediaStore.MediaColumns import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.withContext -import timber.log.Timber +import java.io.BufferedOutputStream import java.io.File -import java.io.OutputStream -import java.time.Instant +import java.io.FileOutputStream import java.util.zip.ZipEntry import java.util.zip.ZipOutputStream @@ -22,73 +16,60 @@ class FileUtils( private val ioDispatcher: CoroutineDispatcher, ) { - fun createWgFiles(tunnels: TunnelConfigs): List { - return tunnels.map { config -> - val file = File(context.cacheDir, "${config.name}-wg.conf") - file.outputStream().use { - it.write(config.wgQuick.toByteArray()) - } - file - } - } - - fun createAmFiles(tunnels: TunnelConfigs): List { - return tunnels.filter { it.amQuick != TunnelConfig.AM_QUICK_DEFAULT }.map { config -> - val file = File(context.cacheDir, "${config.name}-am.conf") - file.outputStream().use { - it.write(config.amQuick.toByteArray()) - } - file - } - } - - suspend fun saveFilesToZip(files: List): Result { + suspend fun createWgFiles(tunnels: TunnelConfigs): List { return withContext(ioDispatcher) { - try { - val zipOutputStream = - createDownloadsFileOutputStream( - "wg-export_${Instant.now().epochSecond}.zip", - Constants.ZIP_FILE_MIME_TYPE, - ) - ZipOutputStream(zipOutputStream).use { zos -> - files.forEach { file -> - val entry = ZipEntry(file.name) - zos.putNextEntry(entry) - if (file.isFile) { - file.inputStream().use { fis -> fis.copyTo(zos) } + tunnels.map { config -> + val file = File(context.cacheDir, "${config.name}-wg.conf") + file.outputStream().use { + it.write(config.wgQuick.toByteArray()) + } + file + } + } + } + + suspend fun createAmFiles(tunnels: TunnelConfigs): List { + return withContext(ioDispatcher) { + tunnels.filter { it.amQuick != TunnelConfig.AM_QUICK_DEFAULT }.map { config -> + val file = File(context.cacheDir, "${config.name}-am.conf") + file.outputStream().use { + it.write(config.amQuick.toByteArray()) + } + file + } + } + } + + suspend fun zipAll(zipFile: File, files: List) { + withContext(ioDispatcher) { + ZipOutputStream(BufferedOutputStream(FileOutputStream(zipFile))).use { zos -> + files.forEach { file -> + val zipFileName = ( + file.parentFile?.let { parent -> + file.absolutePath.removePrefix(parent.absolutePath) + } ?: file.absolutePath + ).removePrefix("/") + val entry = ZipEntry("$zipFileName${(if (file.isDirectory) "/" else "")}") + zos.putNextEntry(entry) + if (file.isFile) { + file.inputStream().use { + it.copyTo(zos) } } - return@withContext Result.success(Unit) } - } catch (e: Exception) { - Timber.e(e) - Result.failure(ConfigExportException) } } } - // TODO issue with android 9 - private fun createDownloadsFileOutputStream(fileName: String, mimeType: String = Constants.ALL_FILE_TYPES): OutputStream? { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - val resolver = context.contentResolver - val contentValues = - ContentValues().apply { - put(MediaColumns.DISPLAY_NAME, fileName) - put(MediaColumns.MIME_TYPE, mimeType) - put(MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS) - } - val uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues) - if (uri != null) { - return resolver.openOutputStream(uri) - } - } else { - val target = - File( - Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), - fileName, - ) - return target.outputStream() + suspend fun createNewShareFile(name: String): File { + return withContext(ioDispatcher) { + val sharePath = File(context.filesDir, "external_files") + if (sharePath.exists()) sharePath.delete() + sharePath.mkdir() + val file = File("${sharePath.path}/$name") + if (file.exists()) file.delete() + file.createNewFile() + file } - return null } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/LocaleUtil.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/LocaleUtil.kt new file mode 100644 index 0000000..3ed091e --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/LocaleUtil.kt @@ -0,0 +1,100 @@ +package com.zaneschepke.wireguardautotunnel.util + +import android.content.Context +import android.content.res.Configuration +import android.content.res.Resources +import android.os.Build +import android.os.LocaleList +import androidx.core.os.ConfigurationCompat +import com.zaneschepke.wireguardautotunnel.BuildConfig +import java.util.Locale + +object LocaleUtil { + private const val DEFAULT_LANG = "en" + val supportedLocales: Array = BuildConfig.LANGUAGES + const val OPTION_PHONE_LANGUAGE = "sys_def" + + /** + * returns the locale to use depending on the preference value + * when preference value = "sys_def" returns the locale of current system + * else it returns the locale code e.g. "en", "bn" etc. + */ + fun getLocaleFromPrefCode(prefCode: String): Locale { + val localeCode = if (prefCode != OPTION_PHONE_LANGUAGE) { + prefCode + } else { + val systemLang = ConfigurationCompat.getLocales(Resources.getSystem().configuration).get(0)?.language ?: DEFAULT_LANG + if (systemLang in supportedLocales) { + systemLang + } else { + DEFAULT_LANG + } + } + return Locale.forLanguageTag(localeCode) + } + + fun getLocalizedConfiguration(prefLocaleCode: String): Configuration { + val locale = getLocaleFromPrefCode(prefLocaleCode) + return getLocalizedConfiguration(locale) + } + + private fun getLocalizedConfiguration(locale: Locale): Configuration { + val config = Configuration() + return config.apply { + config.setLayoutDirection(locale) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + config.setLocale(locale) + val localeList = LocaleList(locale) + LocaleList.setDefault(localeList) + config.setLocales(localeList) + } else { + config.setLocale(locale) + } + } + } + + fun getLocalizedContext(baseContext: Context, prefLocaleCode: String?): Context { + if (prefLocaleCode == null) return baseContext + val currentLocale = getLocaleFromPrefCode(prefLocaleCode) + val baseLocale = getLocaleFromConfiguration(baseContext.resources.configuration) + Locale.setDefault(currentLocale) + return if (!baseLocale.toString().equals(currentLocale.toString(), ignoreCase = true)) { + val config = getLocalizedConfiguration(currentLocale) + baseContext.createConfigurationContext(config) + baseContext + } else { + baseContext + } + } + + fun applyLocalizedContext(baseContext: Context, prefLocaleCode: String) { + val currentLocale = getLocaleFromPrefCode(prefLocaleCode) + val baseLocale = getLocaleFromConfiguration(baseContext.resources.configuration) + Locale.setDefault(currentLocale) + if (!baseLocale.toString().equals(currentLocale.toString(), ignoreCase = true)) { + val config = getLocalizedConfiguration(currentLocale) + baseContext.resources.updateConfiguration(config, baseContext.resources.displayMetrics) + } + } + + @Suppress("DEPRECATION") + private fun getLocaleFromConfiguration(configuration: Configuration): Locale { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + configuration.locales.get(0) + } else { + configuration.locale + } + } + + fun getLocalizedResources(resources: Resources, prefLocaleCode: String): Resources { + val locale = getLocaleFromPrefCode(prefLocaleCode) + val config = resources.configuration + @Suppress("DEPRECATION") + config.locale = locale + config.setLayoutDirection(locale) + + @Suppress("DEPRECATION") + resources.updateConfiguration(config, resources.displayMetrics) + return resources + } +} 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 fb73efb..056213c 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 @@ -4,16 +4,24 @@ import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.pm.PackageManager +import android.location.LocationManager import android.net.Uri import android.provider.Settings import android.service.quicksettings.TileService import android.widget.Toast +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 +private const val BASELINE_HEIGHT = 2201 +private const val BASELINE_WIDTH = 1080 +private const val BASELINE_DENSITY = 2.625 + fun Context.openWebUrl(url: String): Result { return kotlin.runCatching { val webpage: Uri = Uri.parse(url) @@ -26,6 +34,44 @@ fun Context.openWebUrl(url: String): Result { } } +val Context.actionBarSize + get() = theme.obtainStyledAttributes(intArrayOf(android.R.attr.actionBarSize)) + .let { attrs -> attrs.getDimension(0, 0F).toInt().also { attrs.recycle() } } + +fun Context.resizeHeight(dp: Dp): Dp { + val displayMetrics = resources.displayMetrics + val density = displayMetrics.density + val height = displayMetrics.heightPixels - this.actionBarSize + val resizeHeightPercentage = + (height.toFloat() / BASELINE_HEIGHT) * (BASELINE_DENSITY.toFloat() / density) + return dp * resizeHeightPercentage +} + +fun Context.resizeHeight(textUnit: TextUnit): TextUnit { + val displayMetrics = resources.displayMetrics + val density = displayMetrics.density + val height = displayMetrics.heightPixels - actionBarSize + val resizeHeightPercentage = + (height.toFloat() / BASELINE_HEIGHT) * (BASELINE_DENSITY.toFloat() / density) + return textUnit * resizeHeightPercentage * 1.1 +} + +fun Context.resizeWidth(dp: Dp): Dp { + val displayMetrics = resources.displayMetrics + val density = displayMetrics.density + val width = displayMetrics.widthPixels + val resizeWidthPercentage = + (width.toFloat() / BASELINE_WIDTH) * (BASELINE_DENSITY.toFloat() / density) + return dp * resizeWidthPercentage +} + +fun Context.launchNotificationSettings() { + val settingsIntent: Intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + .putExtra(Settings.EXTRA_APP_PACKAGE, packageName) + this.startActivity(settingsIntent) +} + fun Context.launchShareFile(file: Uri) { val shareIntent = Intent().apply { setAction(Intent.ACTION_SEND) @@ -36,6 +82,14 @@ fun Context.launchShareFile(file: Uri) { this.startActivity(Intent.createChooser(shareIntent, "")) } +fun Context.isLocationServicesEnabled(): Boolean { + val locationManager = + getSystemService( + Context.LOCATION_SERVICE, + ) as LocationManager + return LocationManagerCompat.isLocationEnabled(locationManager) +} + fun Context.showToast(resId: Int) { Toast.makeText( this, diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/UiExtensions.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/UiExtensions.kt index 646c728..afba1f3 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/UiExtensions.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/UiExtensions.kt @@ -1,6 +1,9 @@ package com.zaneschepke.wireguardautotunnel.util.extensions +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.TextUnit import androidx.navigation.NavController +import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.ui.Route fun NavController.navigateAndForget(route: Route) { @@ -8,3 +11,15 @@ fun NavController.navigateAndForget(route: Route) { popUpTo(0) } } + +fun Dp.scaledHeight(): Dp { + return WireGuardAutoTunnel.instance.resizeHeight(this) +} + +fun Dp.scaledWidth(): Dp { + return WireGuardAutoTunnel.instance.resizeWidth(this) +} + +fun TextUnit.scaled(): TextUnit { + return WireGuardAutoTunnel.instance.resizeHeight(this) +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index acad34c..8cc001b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -200,4 +200,15 @@ sec handshake Logs + Tunnel notifications + Kill switch + Appearance + Notifications + Automatic + Light + Dark + Dynamic + Language + Display theme + Selected diff --git a/buildSrc/src/main/kotlin/Extensions.kt b/buildSrc/src/main/kotlin/Extensions.kt index fb244b1..d53bd38 100644 --- a/buildSrc/src/main/kotlin/Extensions.kt +++ b/buildSrc/src/main/kotlin/Extensions.kt @@ -72,6 +72,18 @@ fun Project.getSigningProperty(property: String): String { ) } +fun Project.languageList(): List { + return fileTree("../app/src/main/res") { include("**/strings.xml") } + .asSequence() + .map { stringFile -> stringFile.parentFile.name } + .map { valuesFolderName -> valuesFolderName.replace("values-", "") } + .filter { valuesFolderName -> valuesFolderName != "values" } + .map { languageCode -> languageCode.replace("-r", "_") } + .distinct() + .sorted() + .toList() + "en" +} +