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"
+}
+