refactor: language selection

fix: amnezia edit bug
closes #425
This commit is contained in:
Zane Schepke 2024-11-23 00:32:30 -05:00
parent a9d5994070
commit f79f922838
14 changed files with 65 additions and 170 deletions

View File

@ -68,7 +68,9 @@
<activity
android:name=".ui.MainActivity"
android:exported="true"
android:theme="@style/Theme.WireguardAutoTunnel">
android:theme="@style/Theme.WireguardAutoTunnel"
android:configChanges="orientation|screenSize|keyboardHidden"
>
<intent-filter>
<action android:name="android.intent.action.MAIN" />

View File

@ -1,11 +1,11 @@
package com.zaneschepke.wireguardautotunnel
import android.app.Application
import android.content.Context
import android.os.StrictMode
import android.os.StrictMode.ThreadPolicy
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.os.LocaleListCompat
import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.wireguardautotunnel.data.datastore.LocaleStorage
import com.zaneschepke.wireguardautotunnel.data.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
@ -22,10 +22,6 @@ import javax.inject.Inject
@HiltAndroidApp
class WireGuardAutoTunnel : Application() {
val localeStorage: LocaleStorage by lazy {
LocaleStorage(this)
}
@Inject
@ApplicationScope
lateinit var applicationScope: CoroutineScope
@ -56,6 +52,13 @@ class WireGuardAutoTunnel : Application() {
} else {
Timber.plant(ReleaseTree())
}
applicationScope.launch {
appStateRepository.getLocale()?.let {
val locale = LocaleUtil.getLocaleFromPrefCode(it)
val appLocale: LocaleListCompat = LocaleListCompat.forLanguageTags(locale)
AppCompatDelegate.setApplicationLocales(appLocale)
}
}
if (!isRunningOnTv()) {
applicationScope.launch(ioDispatcher) {
if (appStateRepository.isLocalLogsEnabled()) {
@ -66,10 +69,6 @@ class WireGuardAutoTunnel : Application() {
}
}
override fun attachBaseContext(base: Context) {
super.attachBaseContext(LocaleUtil.getLocalizedContext(base, LocaleStorage(base).getPreferredLocale()))
}
companion object {
lateinit var instance: WireGuardAutoTunnel
private set

View File

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

View File

@ -1,17 +0,0 @@
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()
}
}

View File

@ -8,6 +8,7 @@ data class GeneralState(
val isPinLockEnabled: Boolean = PIN_LOCK_ENABLED_DEFAULT,
val isTunnelStatsExpanded: Boolean = IS_TUNNEL_STATS_EXPANDED,
val isLocalLogsEnabled: Boolean = IS_LOGS_ENABLED_DEFAULT,
val locale: String? = null,
val theme: Theme = Theme.AUTOMATIC,
) {
companion object {

View File

@ -33,5 +33,9 @@ interface AppStateRepository {
suspend fun setLocalLogsEnabled(enabled: Boolean)
suspend fun setLocale(localeTag: String)
suspend fun getLocale(): String?
val generalStateFlow: Flow<GeneralState>
}

View File

@ -77,6 +77,14 @@ class DataStoreAppStateRepository(
dataStoreManager.saveToDataStore(DataStoreManager.isLocalLogsEnabled, enabled)
}
override suspend fun setLocale(localeTag: String) {
dataStoreManager.saveToDataStore(DataStoreManager.locale, localeTag)
}
override suspend fun getLocale(): String? {
return dataStoreManager.getFromStore(DataStoreManager.locale)
}
override val generalStateFlow: Flow<GeneralState> =
dataStoreManager.preferencesFlow.map { prefs ->
prefs?.let { pref ->
@ -93,6 +101,7 @@ class DataStoreAppStateRepository(
?: GeneralState.PIN_LOCK_ENABLED_DEFAULT,
isTunnelStatsExpanded = pref[DataStoreManager.tunnelStatsExpanded] ?: GeneralState.IS_TUNNEL_STATS_EXPANDED,
isLocalLogsEnabled = pref[DataStoreManager.isLocalLogsEnabled] ?: GeneralState.IS_LOGS_ENABLED_DEFAULT,
locale = pref[DataStoreManager.locale],
theme = getTheme(),
)
} catch (e: IllegalArgumentException) {

View File

@ -1,5 +1,7 @@
package com.zaneschepke.wireguardautotunnel.ui
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.os.LocaleListCompat
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.wireguard.android.backend.WgQuickBackend
@ -15,6 +17,8 @@ import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.LocaleUtil
import com.zaneschepke.wireguardautotunnel.util.LocaleUtil.OPTION_PHONE_LANGUAGE
import com.zaneschepke.wireguardautotunnel.util.StringValue
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineDispatcher
@ -147,6 +151,14 @@ constructor(
}
}
fun onLocaleChange(localeTag: String) = viewModelScope.launch {
val locale = LocaleUtil.getLocaleFromPrefCode(localeTag)
val storageLocale = if (localeTag == OPTION_PHONE_LANGUAGE) OPTION_PHONE_LANGUAGE else locale
appDataRepository.appState.setLocale(storageLocale)
val appLocale: LocaleListCompat = LocaleListCompat.forLanguageTags(locale)
AppCompatDelegate.setApplicationLocales(appLocale)
}
fun onToggleRestartAtBoot() = viewModelScope.launch {
with(uiState.value.settings) {
appDataRepository.settings.save(

View File

@ -1,6 +1,5 @@
package com.zaneschepke.wireguardautotunnel.ui
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.activity.compose.setContent
@ -35,8 +34,6 @@ import androidx.navigation.compose.composable
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.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
@ -59,7 +56,6 @@ 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 dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@ -68,12 +64,6 @@ import kotlin.system.exitProcess
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val localeStorage: LocaleStorage by lazy {
(application as WireGuardAutoTunnel).localeStorage
}
private lateinit var oldPrefLocaleCode: String
@Inject
lateinit var appStateRepository: AppStateRepository
@ -185,7 +175,7 @@ class MainActivity : AppCompatActivity() {
AppearanceScreen()
}
composable<Route.Language> {
LanguageScreen(localeStorage)
LanguageScreen(appUiState, viewModel)
}
composable<Route.Display> {
DisplayScreen(appUiState)
@ -225,19 +215,4 @@ 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()
}
}

View File

@ -25,7 +25,6 @@ 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) {

View File

@ -90,8 +90,8 @@ fun ConfigScreen(tunnelId: Int) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
var configType by remember { mutableStateOf<ConfigType?>(null) }
val derivedConfigType = remember {
derivedStateOf<ConfigType> {
configType ?: if (!uiState.hasAmneziaProperties()) ConfigType.WIREGUARD else ConfigType.AMNEZIA
derivedStateOf {
configType ?: if (!uiState.isAmneziaEnabled) ConfigType.WIREGUARD else ConfigType.AMNEZIA
}
}
val saved by viewModel.saved.collectAsStateWithLifecycle(null)
@ -181,8 +181,8 @@ fun ConfigScreen(tunnelId: Int) {
)
}
},
) {
Column(Modifier.padding(it)) {
) { padding ->
Column(Modifier.padding(padding)) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top,
@ -243,12 +243,12 @@ fun ConfigScreen(tunnelId: Int) {
.clickable { showAuthPrompt = true },
value = uiState.interfaceProxy.privateKey,
visualTransformation =
if ((tunnelId == Constants.MANUAL_TUNNEL_CONFIG_ID.toInt()) || isAuthenticated) {
if ((tunnelId == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated) {
VisualTransformation.None
} else {
PasswordVisualTransformation()
},
enabled = (tunnelId == Constants.MANUAL_TUNNEL_CONFIG_ID.toInt()) || isAuthenticated,
enabled = (tunnelId == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated,
onValueChange = { value -> viewModel.onPrivateKeyChange(value) },
trailingIcon = {
IconButton(

View File

@ -18,9 +18,6 @@ data class ConfigUiState(
var tunnelName: String = "",
val isAmneziaEnabled: Boolean = false,
) {
fun hasAmneziaProperties(): Boolean {
return this.interfaceProxy.junkPacketCount != ""
}
companion object {
fun from(config: Config): ConfigUiState {
val proxyPeers = config.peers.map { PeerProxy.from(it) }
@ -77,6 +74,7 @@ data class ConfigUiState(
return from(config).copy(
tunnelName = tunnel.name,
tunnel = tunnel,
isAmneziaEnabled = config.`interface`.junkPacketCount.isPresent,
)
}
}

View File

@ -11,39 +11,27 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Scaffold
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.AppUiState
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
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.ui.common.navigation.TopNavBar
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
fun LanguageScreen(appUiState: AppUiState, appViewModel: AppViewModel) {
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)
@ -54,28 +42,17 @@ fun LanguageScreen(localeStorage: LocaleStorage) {
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)
}
Scaffold(
topBar = {
TopNavBar(stringResource(R.string.language))
},
) {
) { padding ->
LazyColumn(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top,
modifier =
Modifier
.fillMaxSize().padding(it)
.fillMaxSize().padding(padding)
.padding(horizontal = 24.dp.scaledWidth()).windowInsetsPadding(WindowInsets.navigationBars),
) {
item {
@ -83,10 +60,10 @@ fun LanguageScreen(localeStorage: LocaleStorage) {
SelectionItemButton(
buttonText = stringResource(R.string.automatic),
onClick = {
onChangeLocale(LocaleUtil.OPTION_PHONE_LANGUAGE)
appViewModel.onLocaleChange(LocaleUtil.OPTION_PHONE_LANGUAGE)
},
trailing = {
if (currentLocale.value == LocaleUtil.OPTION_PHONE_LANGUAGE) {
if (appUiState.generalState.locale == LocaleUtil.OPTION_PHONE_LANGUAGE) {
SelectedLabel()
}
},
@ -96,13 +73,18 @@ fun LanguageScreen(localeStorage: LocaleStorage) {
}
items(sortedLocales, key = { it }) { locale ->
SelectionItemButton(
buttonText = locale.getDisplayLanguage(locale).capitalize(locale) +
if (locale.toLanguageTag().contains("-")) " (${locale.getDisplayCountry(locale).capitalize(locale)})" else "",
buttonText = locale.getDisplayLanguage(locale).replaceFirstChar { if (it.isLowerCase()) it.titlecase(locale) else it.toString() } +
if (locale.toLanguageTag().contains("-")) {
" (${locale.getDisplayCountry(locale)
.replaceFirstChar { if (it.isLowerCase()) it.titlecase(locale) else it.toString() }})"
} else {
""
},
onClick = {
onChangeLocale(locale.toLanguageTag())
appViewModel.onLocaleChange(locale.toLanguageTag())
},
trailing = {
if (locale.toLanguageTag() == currentLocale.value) {
if (locale.toLanguageTag() == appUiState.generalState.locale) {
SelectedLabel()
}
},

View File

@ -1,13 +1,8 @@
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"
@ -19,7 +14,7 @@ object LocaleUtil {
* 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 {
fun getLocaleFromPrefCode(prefCode: String): String {
val localeCode = if (prefCode != OPTION_PHONE_LANGUAGE) {
prefCode
} else {
@ -30,71 +25,6 @@ object LocaleUtil {
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
return localeCode
}
}